dynapydantic


Namedynapydantic JSON
Version 0.1.1 PyPI version JSON
download
home_pageNone
SummaryDyanmic pydantic models
upload_time2025-07-11 20:37:40
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseNone
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # dynapydantic

[![CI](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml)
[![Pre-commit](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml)
[![Docs](https://img.shields.io/badge/docs-Docs-blue?style=flat-square&logo=github&logoColor=white&link=https://psalvaggio.github.io/dynapydantic/dev/)](https://psalvaggio.github.io/dynapydantic/dev/)


`dynapydantic` is an extension to the [pydantic](https://pydantic.dev) Python
package that allow for dynamic tracking of `pydantic.BaseModel` subclasses.

Installation
==
This project can be installed via PyPI:
```
pip install dynapydantic
```

Usage
==

`TrackingGroup`
--
The core entity in this library is the `dynapydantic.TrackingGroup`:
```python
import typing as ty

import dynapydantic
import pydantic

mygroup = dynapydantic.TrackingGroup(
    name="mygroup",
    discriminator_field="name"
)

@mygroup.register("A")
class A(pydantic.BaseModel):
    """A class to be tracked, will be tracked as "A"."""
    a: int

@mygroup.register()
class B(pydantic.BaseModel):
    """Another class, will be tracked as "B"."""
    name: ty.Literal["B"] = "B"
    a: int

class Model(pydantic.BaseModel):
    """A model that can have A or B"""
    field: mygroup.union()  # call after all subclasses have been registered

print(Model(field={"name": "A", "a": 4})) # field=A(a=4, name='A')
print(Model(field={"name": "B", "a": 5})) # field=B(name='B', a=5)
```

The `union()` method produces a [discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions)
of all registered `pydantic.BaseModel` subclasses. It also accepts an
`annotated=False` keyword argument to produce a plain `typing.Union` for use in
type annotations. This union is based on a discriminator field, which was
configured by the `discriminator_field` argument to `TrackingGroup`. The field
can be created by hand, as was shown with `B`, or `dynapydantic` will inject it
for you, as was shown with `A`.

`TrackingGroup` has a few opt-in features to make it more powerful and easier to use:
1. `discriminator_value_generator`: This parameter is a optional callback
  function that is called with each class that gets registered and produces a
  default value for the discriminator field. This allows the user to call
  `register()` without a value for the discriminator. The most common value to
  pass here would be `lambda cls: cls.__name__`, to use the name of the class as
  the discriminator value.
2. `plugin_entry_point`: This parameter indicates to `dynapydantic` that there
  might be models to be discovered in other packages. Packages are discovered by
  the Python entrypoint mechanism. See the `tests/example` directory for an
  example of how this works.

`SubclassTrackingModel`
--
The most common use case of this pattern is to automatically register subclasses
of a given `pydantic.BaseModel`. This is supported via the use of
`dynapydantic.SubclassTrackingModel`. For example:
```python
import typing as ty

import dynapydantic
import pydantic

class Base(
    dynapydantic.SubclassTrackingModel,
    discriminator_field="name",
    discriminator_value_generator=lambda cls: cls.__name__,
):
    """Base model, will track its subclasses"""

    # The TrackingGroup can be specified here like model_config, or passed in
    # kwargs of the class declaration, just like how model_config works with
    # pydantic.BaseModel. If you do it like this, you have to give the tracking
    # group a name
    # tracking_config: ty.ClassVar[dynapydantic.TrackingGroup] = dynapydantic.TrackingGroup(
    #     name="BaseSubclasses",
    #     discriminator_field="name",
    #     discriminator_value_generator=lambda cls: cls.__name__,
    # )


class Intermediate(Base, exclude_from_union=True):
    """Subclasses can opt out of being tracked"""

class Derived1(Intermediate):
    """Non-direct descendants are registered"""
    a: int

class Derived2(Intermediate):
    """You can override the value generator if desired"""
    name: ty.Literal["Custom"] = "Custom"
    a: int

print(Base.registered_subclasses())
# {'Derived1': <class '__main__.Derived1'>, 'Custom': <class '__main__.Derived2'>}

# if plugin_entry_point was specificed, load plugin packages
# Base.load_plugins()

class Model(pydantic.BaseModel):
    """A model that can have any registered Base subclass"""
    field: Base.union()  # call after all subclasses have been registered

print(Model(field={"name": "Derived1", "a": 4}))
# field=Derived1(a=4, name='Derived1')
print(Model(field={"name": "Custom", "a": 5}))
# field=Derived2(name='Custom', a=5)
```

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "dynapydantic",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": "Philip Salvaggio <salvaggio.philip@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/9a/f6/5ee0376aaac2f289d6117799a0e2aa7b01296702383e7f03da12215d6dff/dynapydantic-0.1.1.tar.gz",
    "platform": null,
    "description": "# dynapydantic\n\n[![CI](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml)\n[![Pre-commit](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml)\n[![Docs](https://img.shields.io/badge/docs-Docs-blue?style=flat-square&logo=github&logoColor=white&link=https://psalvaggio.github.io/dynapydantic/dev/)](https://psalvaggio.github.io/dynapydantic/dev/)\n\n\n`dynapydantic` is an extension to the [pydantic](https://pydantic.dev) Python\npackage that allow for dynamic tracking of `pydantic.BaseModel` subclasses.\n\nInstallation\n==\nThis project can be installed via PyPI:\n```\npip install dynapydantic\n```\n\nUsage\n==\n\n`TrackingGroup`\n--\nThe core entity in this library is the `dynapydantic.TrackingGroup`:\n```python\nimport typing as ty\n\nimport dynapydantic\nimport pydantic\n\nmygroup = dynapydantic.TrackingGroup(\n    name=\"mygroup\",\n    discriminator_field=\"name\"\n)\n\n@mygroup.register(\"A\")\nclass A(pydantic.BaseModel):\n    \"\"\"A class to be tracked, will be tracked as \"A\".\"\"\"\n    a: int\n\n@mygroup.register()\nclass B(pydantic.BaseModel):\n    \"\"\"Another class, will be tracked as \"B\".\"\"\"\n    name: ty.Literal[\"B\"] = \"B\"\n    a: int\n\nclass Model(pydantic.BaseModel):\n    \"\"\"A model that can have A or B\"\"\"\n    field: mygroup.union()  # call after all subclasses have been registered\n\nprint(Model(field={\"name\": \"A\", \"a\": 4})) # field=A(a=4, name='A')\nprint(Model(field={\"name\": \"B\", \"a\": 5})) # field=B(name='B', a=5)\n```\n\nThe `union()` method produces a [discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions)\nof all registered `pydantic.BaseModel` subclasses. It also accepts an\n`annotated=False` keyword argument to produce a plain `typing.Union` for use in\ntype annotations. This union is based on a discriminator field, which was\nconfigured by the `discriminator_field` argument to `TrackingGroup`. The field\ncan be created by hand, as was shown with `B`, or `dynapydantic` will inject it\nfor you, as was shown with `A`.\n\n`TrackingGroup` has a few opt-in features to make it more powerful and easier to use:\n1. `discriminator_value_generator`: This parameter is a optional callback\n  function that is called with each class that gets registered and produces a\n  default value for the discriminator field. This allows the user to call\n  `register()` without a value for the discriminator. The most common value to\n  pass here would be `lambda cls: cls.__name__`, to use the name of the class as\n  the discriminator value.\n2. `plugin_entry_point`: This parameter indicates to `dynapydantic` that there\n  might be models to be discovered in other packages. Packages are discovered by\n  the Python entrypoint mechanism. See the `tests/example` directory for an\n  example of how this works.\n\n`SubclassTrackingModel`\n--\nThe most common use case of this pattern is to automatically register subclasses\nof a given `pydantic.BaseModel`. This is supported via the use of\n`dynapydantic.SubclassTrackingModel`. For example:\n```python\nimport typing as ty\n\nimport dynapydantic\nimport pydantic\n\nclass Base(\n    dynapydantic.SubclassTrackingModel,\n    discriminator_field=\"name\",\n    discriminator_value_generator=lambda cls: cls.__name__,\n):\n    \"\"\"Base model, will track its subclasses\"\"\"\n\n    # The TrackingGroup can be specified here like model_config, or passed in\n    # kwargs of the class declaration, just like how model_config works with\n    # pydantic.BaseModel. If you do it like this, you have to give the tracking\n    # group a name\n    # tracking_config: ty.ClassVar[dynapydantic.TrackingGroup] = dynapydantic.TrackingGroup(\n    #     name=\"BaseSubclasses\",\n    #     discriminator_field=\"name\",\n    #     discriminator_value_generator=lambda cls: cls.__name__,\n    # )\n\n\nclass Intermediate(Base, exclude_from_union=True):\n    \"\"\"Subclasses can opt out of being tracked\"\"\"\n\nclass Derived1(Intermediate):\n    \"\"\"Non-direct descendants are registered\"\"\"\n    a: int\n\nclass Derived2(Intermediate):\n    \"\"\"You can override the value generator if desired\"\"\"\n    name: ty.Literal[\"Custom\"] = \"Custom\"\n    a: int\n\nprint(Base.registered_subclasses())\n# {'Derived1': <class '__main__.Derived1'>, 'Custom': <class '__main__.Derived2'>}\n\n# if plugin_entry_point was specificed, load plugin packages\n# Base.load_plugins()\n\nclass Model(pydantic.BaseModel):\n    \"\"\"A model that can have any registered Base subclass\"\"\"\n    field: Base.union()  # call after all subclasses have been registered\n\nprint(Model(field={\"name\": \"Derived1\", \"a\": 4}))\n# field=Derived1(a=4, name='Derived1')\nprint(Model(field={\"name\": \"Custom\", \"a\": 5}))\n# field=Derived2(name='Custom', a=5)\n```\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Dyanmic pydantic models",
    "version": "0.1.1",
    "project_urls": null,
    "split_keywords": [],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "2f6304fb795b1852937379a6ebd0746718ece7a92c594e2ed7a0b1aa3a506d75",
                "md5": "78deec23120e71130d84e3f510073dc4",
                "sha256": "958ea7d67de4f35933d79d619ccd5c5970ba38fe455f1f45343a34c91ccb3a7f"
            },
            "downloads": -1,
            "filename": "dynapydantic-0.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "78deec23120e71130d84e3f510073dc4",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 8202,
            "upload_time": "2025-07-11T20:37:39",
            "upload_time_iso_8601": "2025-07-11T20:37:39.309159Z",
            "url": "https://files.pythonhosted.org/packages/2f/63/04fb795b1852937379a6ebd0746718ece7a92c594e2ed7a0b1aa3a506d75/dynapydantic-0.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "9af65ee0376aaac2f289d6117799a0e2aa7b01296702383e7f03da12215d6dff",
                "md5": "1c82b7df9fbad2fa33d4cd23c148c4ff",
                "sha256": "42116241990de5f82a1e47190115f1420e068ef1c553f03935c4023623235a12"
            },
            "downloads": -1,
            "filename": "dynapydantic-0.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "1c82b7df9fbad2fa33d4cd23c148c4ff",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 102912,
            "upload_time": "2025-07-11T20:37:40",
            "upload_time_iso_8601": "2025-07-11T20:37:40.336667Z",
            "url": "https://files.pythonhosted.org/packages/9a/f6/5ee0376aaac2f289d6117799a0e2aa7b01296702383e7f03da12215d6dff/dynapydantic-0.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-07-11 20:37:40",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "dynapydantic"
}
        
Elapsed time: 0.62609s