diff --git a/README.md b/README.md index 0646e29..a59d248 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,53 @@ Model your domain at the edge. > active development, and its API is subject to change. We encourage developers to experiment with DTOS and provide > feedback, but we recommend against using it in production environments until a stable release is available.` +## Additional Goals + +In addition to the primary goals of the library, we are aiming to address the following issues with the Litestar DTO +implementation: + +- Tagged union support: If the type annotation encounters a union field where the inner types are supported by the DTO, +we should have a framework-specific way to elect a discrimination tag for each type. If there is no tag, the annotation +would not be supported. E.g., for SQLAlchemy we'd be able to use the `polymorphic_on` value to specify the tag. +- Transfer model naming: the names of the transfer models manifest in any json schema or openapi documentation. We want +first class support for controlling these names and logical defaults. +- Applying the DTO to the whole annotation: the current Litestar implementation looks for a DTO supported type within +the annotation and binds itself to that. The goal of the DTO should be to produce an encodable object from an instance +of the annotated type, and be able to construct an instance of the annotated type from input data. Types that are +supported by the DTO should be able to be arbitrarily nested within the annotation, and the DTO should be able to +traverse the annotation to find them, and deal with them in place. +- Support multiple modelling libraries: In Litestar, a DTO is bound to a single modelling library via inheritance. We +should be able to support multiple modelling libraries in the same DTO object by using a plugin system instead of +inheritance. +- Global configuration: Support binding config objects to model types so that the same model can share a config object +across multiple DTOs. This would be especially useful for types that nest models within them. E.g., something like: + +```python +from dataclasses import dataclass + +from dtos import DTOConfig, register_config + +@dataclass +class Base: + secret_thing: str + +@dataclass +class A(Base): ... + +# this config would apply to all models that inherit from Base, unless a more specific config is provided +register_config(Base, DTOConfig(rename="camel", exclude={"secret_thing"})) +``` +- First class support for generic types: The following doesn't work in Litestar: + +```python +@dataclass +class Foo(Generic[T]): + foo: T + + +FooDTO = DataclassDTO[Foo[int]] +``` + ## About The `dtos` library bridges the gap between complex domain models and their practical usage across network boundaries. diff --git a/docs/PYPI_README.md b/docs/PYPI_README.md index 9b9082c..662f265 100644 --- a/docs/PYPI_README.md +++ b/docs/PYPI_README.md @@ -13,12 +13,60 @@ Model your domain at the edge. -> **Warning**: Pre-Release Alpha Stage +> [!WARNING] +> **Pre-Release Alpha Stage** > > Please note that DTOS is currently in a pre-release alpha stage of development. This means the library is still under > active development, and its API is subject to change. We encourage developers to experiment with DTOS and provide > feedback, but we recommend against using it in production environments until a stable release is available.` +## Additional Goals + +In addition to the primary goals of the library, we are aiming to address the following issues with the Litestar DTO +implementation: + +- Tagged union support: If the type annotation encounters a union field where the inner types are supported by the DTO, +we should have a framework-specific way to elect a discrimination tag for each type. If there is no tag, the annotation +would not be supported. E.g., for SQLAlchemy we'd be able to use the `polymorphic_on` value to specify the tag. +- Transfer model naming: the names of the transfer models manifest in any json schema or openapi documentation. We want +first class support for controlling these names and logical defaults. +- Applying the DTO to the whole annotation: the current Litestar implementation looks for a DTO supported type within +the annotation and binds itself to that. The goal of the DTO should be to produce an encodable object from an instance +of the annotated type, and be able to construct an instance of the annotated type from input data. Types that are +supported by the DTO should be able to be arbitrarily nested within the annotation, and the DTO should be able to +traverse the annotation to find them, and deal with them in place. +- Support multiple modelling libraries: In Litestar, a DTO is bound to a single modelling library via inheritance. We +should be able to support multiple modelling libraries in the same DTO object by using a plugin system instead of +inheritance. +- Global configuration: Support binding config objects to model types so that the same model can share a config object +across multiple DTOs. This would be especially useful for types that nest models within them. E.g., something like: + +```python +from dataclasses import dataclass + +from dtos import DTOConfig, register_config + +@dataclass +class Base: + secret_thing: str + +@dataclass +class A(Base): ... + +# this config would apply to all models that inherit from Base, unless a more specific config is provided +register_config(Base, DTOConfig(rename="camel", exclude={"secret_thing"})) +``` +- First class support for generic types: The following doesn't work in Litestar: + +```python +@dataclass +class Foo(Generic[T]): + foo: T + + +FooDTO = DataclassDTO[Foo[int]] +``` + ## About The `dtos` library bridges the gap between complex domain models and their practical usage across network boundaries. diff --git a/dtos/__init__.py b/dtos/__init__.py index a3c98e0..d93dfd0 100644 --- a/dtos/__init__.py +++ b/dtos/__init__.py @@ -1,7 +1,68 @@ from __future__ import annotations -__all__ = ("return_three",) +from typing import TYPE_CHECKING, TypeVar +from dtos.config import DTOConfig +from dtos.dto import DTO +from dtos.internals.config_registry import global_config_registry +from dtos.internals.plugin_registry import global_plugin_registry +from dtos.plugins import DataclassPlugin, MsgspecPlugin -def return_three() -> int: - return 3 +if TYPE_CHECKING: + from collections.abc import Sequence + + from dtos.plugins import Plugin + +__all__ = ( + "DTOConfig", + "create_dto", + "register_config", + "register_plugins", +) + +T = TypeVar("T") + + +def register_config(type_: type, config: DTOConfig) -> None: + """Register a global DTO configuration object. + + Args: + type_: The type of the DTO object. + config: The DTO configuration object. + """ + global_config_registry.register(type_, config) + + +def register_plugins(plugins: Sequence[Plugin]) -> None: + """Register a global DTO plugin. + + Args: + plugins: Instances of :class:`Plugin` for the :class:`DTO` instance. Additional to any + plugins already registered. The order of the plugins is important, the first plugin + that can handle a type is used, and plugins registered later are checked first. + """ + global_plugin_registry.register(plugins) + + +def create_dto( + type_: type[T], + plugins: Sequence[Plugin] = (), + type_configs: Sequence[tuple[type, DTOConfig]] = (), +) -> DTO[T]: + """Create a new DTOFactory with the given configurations added. + + Args: + type_: The type of the DTO object. + plugins: Instances of :class:`Plugin` for the :class:`DTO` instance. Additional to, and take + precedence over plugins registered globally. + type_configs: A sequence of tuples where the first element is a :class:`type` and the + second element is a :class:`DTOConfig` instance. Additional to the configurations + registered globally. Types are matched according MRO, longest match is used. + + Returns: + A new :class:`DTO` instance. + """ + return DTO(type_, plugins=plugins, type_configs=type_configs) + + +register_plugins([DataclassPlugin(), MsgspecPlugin()]) diff --git a/dtos/config.py b/dtos/config.py new file mode 100644 index 0000000..7c393c9 --- /dev/null +++ b/dtos/config.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import msgspec + +from dtos.exc import ConfigError + +if TYPE_CHECKING: + from collections.abc import Set + + from dtos.types import RenameStrategy + +__all__ = ("DTOConfig",) + + +class DTOConfig(msgspec.Struct, frozen=True): + """Control the generated DTO.""" + + exclude: Set[str] = msgspec.field(default_factory=set) + """Explicitly exclude fields from the generated DTO. + + If exclude is specified, all fields not specified in exclude will be included by default. + + Notes: + - The field names are dot-separated paths to nested fields, e.g. ``"address.street"`` will + exclude the ``"street"`` field from a nested ``"address"`` model. + - 'exclude' mutually exclusive with 'include' - specifying both values will raise an + ``ImproperlyConfiguredException``. + """ + include: Set[str] = msgspec.field(default_factory=set) + """Explicitly include fields in the generated DTO. + + If include is specified, all fields not specified in include will be excluded by default. + + Notes: + - The field names are dot-separated paths to nested fields, e.g. ``"address.street"`` will + include the ``"street"`` field from a nested ``"address"`` model. + - 'include' mutually exclusive with 'exclude' - specifying both values will raise an + ``ImproperlyConfiguredException``. + """ + rename_fields: dict[str, str] = msgspec.field(default_factory=dict) + """Mapping of field names, to new name.""" + rename_strategy: RenameStrategy | None = None + """Rename all fields using a pre-defined strategy or a custom strategy. + + The pre-defined strategies are: `upper`, `lower`, `camel`, `pascal`. + + A custom strategy is any callable that accepts a string as an argument and + return a string. + + Fields defined in ``rename_fields`` are ignored.""" + max_nested_depth: int = 1 + """The maximum depth of nested items allowed for data transfer.""" + partial: bool = False + """Allow transfer of partial data.""" + underscore_fields_private: bool = True + """Fields starting with an underscore are considered private and excluded from data transfer.""" + + def __post_init__(self) -> None: + if self.include and self.exclude: + msg = "Cannot specify both 'include' and 'exclude' in DTOConfig" + raise ConfigError(msg) diff --git a/dtos/dto.py b/dtos/dto.py new file mode 100644 index 0000000..75400d0 --- /dev/null +++ b/dtos/dto.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, TypeVar + +from type_lens.type_view import TypeView + +from dtos.internals.config_registry import global_config_registry +from dtos.internals.plugin_registry import global_plugin_registry + +if TYPE_CHECKING: + from collections.abc import Sequence + + from dtos.config import DTOConfig + from dtos.plugins import Plugin + +__all__ = ("DTO",) + + +T = TypeVar("T") + + +class DTO(Generic[T]): + __slots__ = { + "type_view": "The :class:`TypeView` of the annotation.", + "_plugin_registry": "Registry for plugins.", + "_config_registry": "Registry for config objects.", + } + + def __init__( + self, + annotation: type[T], + *, + plugins: Sequence[Plugin] = (), + type_configs: Sequence[tuple[type, DTOConfig]] = (), + ) -> None: + self.type_view = TypeView(annotation) + + self._plugin_registry = global_plugin_registry.copy() + self._plugin_registry.register(plugins) + self._config_registry = global_config_registry.copy() + for type_, config in type_configs: + self._config_registry.register(type_, config) diff --git a/dtos/exc.py b/dtos/exc.py new file mode 100644 index 0000000..6bc82a6 --- /dev/null +++ b/dtos/exc.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +__all__ = ( + "ConfigError", + "DtosError", +) + + +class DtosError(Exception): + """Base class for exceptions in the ``dtos`` library.""" + + +class ConfigError(DtosError): + """Raised when there is an error with the configuration of a DTO.""" diff --git a/dtos/internals/__init__.py b/dtos/internals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dtos/internals/config_registry.py b/dtos/internals/config_registry.py new file mode 100644 index 0000000..b086fe8 --- /dev/null +++ b/dtos/internals/config_registry.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dtos.config import DTOConfig + +__all__ = ("global_config_registry",) + + +class ConfigRegistry: + """A global registry of DTO configuration objects.""" + + __slots__ = ( + "configs", + "_cache", + ) + + def __init__(self) -> None: + self.configs: dict[tuple[type, ...], DTOConfig] = {} + self._cache: dict[type, DTOConfig] = {} + + def register(self, type_: type, config: DTOConfig) -> None: + """Register a DTO configuration object. + + Args: + type_ (type): The type of the DTO object. + config (DTOConfig): The DTO configuration object. + """ + self.configs[type_.__mro__] = config + + def get(self, type_: type) -> DTOConfig | None: + """Get the DTO configuration object for the given type. + + Args: + type_ (type): The type of the DTO object. + + Returns: + DTOConfig | None: The DTO configuration object for the given type, or None if not found. + """ + if config := self._cache.get(type_): + return config + + for i in range(1, len(type_.__mro__)): + if config := self.configs.get(type_.__mro__[i:]): + self._cache[type_] = config + return config + return None + + def copy(self) -> ConfigRegistry: + """Create a new ConfigRegistry with the given configurations added. + + Args: + configs (dict[type, DTOConfig] | Sequence[tuple[type, DTOConfig]]): The configurations to add. + + Returns: + ConfigRegistry: A new ConfigRegistry with the given configurations added. + """ + new_registry = ConfigRegistry() + new_registry.configs = self.configs.copy() + return new_registry + + +global_config_registry = ConfigRegistry() diff --git a/dtos/internals/plugin_registry.py b/dtos/internals/plugin_registry.py new file mode 100644 index 0000000..cce50f1 --- /dev/null +++ b/dtos/internals/plugin_registry.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections import deque +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + + from type_lens import TypeView + + from dtos.plugins import Plugin + +__all__ = ("global_plugin_registry",) + + +class PluginRegistry: + """A global registry of DTO plugins.""" + + __slots__ = ("plugins", "_cache") + + def __init__(self) -> None: + self.plugins: deque[Plugin] = deque() + self._cache: dict[TypeView, Plugin] = {} + + def register(self, plugins: Sequence[Plugin]) -> None: + """Register a DTO plugin. + + Args: + plugins: Instances of :class:`Plugin` to register. + """ + self.plugins.extendleft(reversed(plugins)) + + def get(self, type_view: TypeView) -> Plugin | None: + """Get the plugin that can handle the given type. + + Args: + type_view (TypeView): The type to get the plugin for. + + Returns: + Plugin | None: The plugin that can handle the given type, or None if not found. + """ + if plugin := self._cache.get(type_view): + return plugin + + for plugin in self.plugins: + if plugin.can_handle(type_view): + return plugin + return None + + def copy(self) -> PluginRegistry: + """Create a new PluginRegistry with the same plugins. + + Returns: + PluginRegistry: A new PluginRegistry with the same plugins. + """ + new_registry = PluginRegistry() + new_registry.plugins = self.plugins.copy() + return new_registry + + +global_plugin_registry = PluginRegistry() diff --git a/dtos/plugins.py b/dtos/plugins.py new file mode 100644 index 0000000..eb7f499 --- /dev/null +++ b/dtos/plugins.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from type_lens import TypeView + from type_lens.parameter_view import ParameterView + +__all__ = ( + "DataclassPlugin", + "MsgspecPlugin", + "Plugin", +) + + +class Plugin(ABC): + @abstractmethod + def parse_annotation(self, type_view: TypeView) -> tuple[ParameterView, ...]: ... + + @abstractmethod + def can_handle(self, type_view: TypeView) -> bool: ... + + +class DataclassPlugin(Plugin): + def parse_annotation(self, type_view: TypeView) -> tuple[ParameterView, ...]: + return () + + def can_handle(self, type_view: TypeView) -> bool: + return False + + +class MsgspecPlugin(Plugin): + def parse_annotation(self, type_view: TypeView) -> tuple[ParameterView, ...]: + return () + + def can_handle(self, type_view: TypeView) -> bool: + return False diff --git a/dtos/types.py b/dtos/types.py new file mode 100644 index 0000000..24291ce --- /dev/null +++ b/dtos/types.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Literal + + from typing_extensions import TypeAlias + +__all__ = ("RenameStrategy",) + +RenameStrategy: TypeAlias = 'Literal["lower", "upper", "camel", "pascal", "kebab"] | Callable[[str], str]' +"""A pre-defined strategy or a custom callback for converting DTO field names.""" diff --git a/pdm.lock b/pdm.lock index 451f8ac..b663ebc 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "docs", "lint", "test"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:029f5235df941d3f4b5f56cdb5cd95e38c7f7c88ec99ca0d70e71a778aa8874a" +content_hash = "sha256:2d3b258d02afc40021a2170782cb40e00dbb4985e06ea3dd8de11258f476d7f7" [[package]] name = "alabaster" @@ -63,6 +63,16 @@ files = [ {file = "apeye_core-1.1.4.tar.gz", hash = "sha256:72bb89fed3baa647cb81aa28e1d851787edcbf9573853b5d2b5f87c02f50eaf5"}, ] +[[package]] +name = "attrs" +version = "23.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + [[package]] name = "auto-pytabs" version = "0.4.0" @@ -279,7 +289,7 @@ files = [ [[package]] name = "codecov-cli" -version = "0.4.8" +version = "0.4.9" requires_python = ">=3.8" summary = "Codecov Command Line Interface" dependencies = [ @@ -291,10 +301,10 @@ dependencies = [ "tree-sitter==0.20.*", ] files = [ - {file = "codecov-cli-0.4.8.tar.gz", hash = "sha256:427f7f64853b3805cea9f0a65ede711ed7bf9e1f675db4de83bd8dcfc66897ad"}, - {file = "codecov_cli-0.4.8-cp311-cp311-macosx_12_6_x86_64.whl", hash = "sha256:ae76a9cf50714888ebd1192b2254f8ebb6a7de39e6adb9db2abdb10931f12a4c"}, - {file = "codecov_cli-0.4.8-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:7b2db0d86c0f83a9827e31d4858053aab390c8ca2b598a8e2f8bffee3e6b0598"}, - {file = "codecov_cli-0.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:8439fd14d8437a0d2f90eee1e7ab5984a828b4eef0418f99455753ec11880512"}, + {file = "codecov-cli-0.4.9.tar.gz", hash = "sha256:89c4a6b39094dc4181c6797fa9f8cbc2b03cd76038b2f491c3ee030428ee1e0d"}, + {file = "codecov_cli-0.4.9-cp311-cp311-macosx_12_6_x86_64.whl", hash = "sha256:7bfe0ab74fdcc2ace01f5ba2433e770eb8c9cd88ff7c6bbb5a9b4820e279f956"}, + {file = "codecov_cli-0.4.9-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:d18f313235a912398f6bde96c918bbe153ad892d8077e9386302d78ec189faaf"}, + {file = "codecov_cli-0.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:b831ec08da58b8164455bb0fb822c2a31ebd5c991700e6f8024cb296abf463c7"}, ] [[package]] @@ -455,6 +465,16 @@ files = [ {file = "cssutils-2.9.0.tar.gz", hash = "sha256:89477b3d17d790e97b9fb4def708767061055795aae6f7c82ae32e967c9be4cd"}, ] +[[package]] +name = "decorator" +version = "5.1.1" +requires_python = ">=3.5" +summary = "Decorators for Humans" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "dict2css" version = "0.3.0.post1" @@ -722,6 +742,35 @@ files = [ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] +[[package]] +name = "jsonschema" +version = "4.21.1" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +dependencies = [ + "attrs>=22.2.0", + "jsonschema-specifications>=2023.03.6", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +requires_python = ">=3.8" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +dependencies = [ + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + [[package]] name = "livereload" version = "2.6.3" @@ -1022,15 +1071,15 @@ files = [ [[package]] name = "pyright" -version = "1.1.355" +version = "1.1.356" requires_python = ">=3.7" summary = "Command line wrapper for pyright" dependencies = [ "nodeenv>=1.6.0", ] files = [ - {file = "pyright-1.1.355-py3-none-any.whl", hash = "sha256:bf30b6728fd68ae7d09c98292b67152858dd89738569836896df786e52b5fe48"}, - {file = "pyright-1.1.355.tar.gz", hash = "sha256:dca4104cd53d6484e6b1b50b7a239ad2d16d2ffd20030bcf3111b56f44c263bf"}, + {file = "pyright-1.1.356-py3-none-any.whl", hash = "sha256:a101b0f375f93d7082f9046cfaa7ba15b7cf8e1939ace45e984c351f6e8feb99"}, + {file = "pyright-1.1.356.tar.gz", hash = "sha256:f05b8b29d06b96ed4a0885dad5a31d9dff691ca12b2f658249f583d5f2754021"}, ] [[package]] @@ -1065,6 +1114,27 @@ files = [ {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] +[[package]] +name = "pytest-mypy-plugins" +version = "3.1.1" +requires_python = ">=3.8" +summary = "pytest plugin for writing tests for mypy plugins" +dependencies = [ + "Jinja2", + "decorator", + "jsonschema", + "mypy>=1.3", + "packaging", + "pytest>=7.0.0", + "pyyaml", + "regex", + "tomlkit>=0.11", +] +files = [ + {file = "pytest-mypy-plugins-3.1.1.tar.gz", hash = "sha256:968dbfafb7cce247571f53cfe6e4e7d947ab2f6424082a097a98d546c1669be0"}, + {file = "pytest_mypy_plugins-3.1.1-py3-none-any.whl", hash = "sha256:7e4bac8843db9a39f035ab7d9d18bb63d54fd54c3217a18e131ac50e41450847"}, +] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1111,6 +1181,91 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "referencing" +version = "0.34.0" +requires_python = ">=3.8" +summary = "JSON Referencing + Python" +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", +] +files = [ + {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, + {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, +] + +[[package]] +name = "regex" +version = "2023.12.25" +requires_python = ">=3.7" +summary = "Alternative regular expression module, to replace re." +files = [ + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -1164,6 +1319,100 @@ files = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] +[[package]] +name = "rpds-py" +version = "0.18.0" +requires_python = ">=3.8" +summary = "Python bindings to Rust's persistent data structures (rpds)" +files = [ + {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, + {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, + {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, + {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, + {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, + {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, + {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, + {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, + {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, + {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, + {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, +] + [[package]] name = "ruamel-yaml" version = "0.18.5" @@ -1571,6 +1820,16 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tomlkit" +version = "0.12.4" +requires_python = ">=3.7" +summary = "Style preserving TOML library" +files = [ + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + [[package]] name = "tornado" version = "6.3.3" @@ -1665,12 +1924,12 @@ files = [ [[package]] name = "type-lens" -version = "0.1.0a0" +version = "0.1.0a1" requires_python = "<4.0,>=3.9" summary = "type-lens is a Python template project designed to simplify the setup of a new project." files = [ - {file = "type_lens-0.1.0a0-py3-none-any.whl", hash = "sha256:f3a6ba25848cad8a9d0ce1ab066c2b357cec275415d7d92b65a354e23a973e65"}, - {file = "type_lens-0.1.0a0.tar.gz", hash = "sha256:6685694470493f54ee9fb8a53e428db29791b3fb8d02a56527266d13781b906a"}, + {file = "type_lens-0.1.0a1-py3-none-any.whl", hash = "sha256:8aea94c1d4150a6f573216e481e2bcd31649ddd1893fdb9aa736d94d61eb249a"}, + {file = "type_lens-0.1.0a1.tar.gz", hash = "sha256:bd3067e34356045867aba2b6d9f3e42b4ffd55d6bba5c1f4fdd681e723537641"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 209c522..834696a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ readme = "README.md" license = {text = "MIT"} requires-python = ">=3.9,<4.0" dependencies = [ - "type-lens==0.1.0a0", + "type-lens==0.1.0a1", "msgspec>=0.18.6", ] @@ -80,6 +80,7 @@ test = [ "covdefaults", "pytest", "pytest-cov", + "pytest-mypy-plugins", ] [tool.pdm.scripts] @@ -105,7 +106,7 @@ fail-under = 100 [tool.pytest.ini_options] addopts = "--strict-markers --strict-config" -testpaths = ["tests"] +testpaths = ["tests", "typesafety"] xfail_strict = true [tool.mypy] diff --git a/tests/test_interface.py b/tests/test_interface.py new file mode 100644 index 0000000..9d48db4 --- /dev/null +++ b/tests/test_interface.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/tests/test_lib.py b/tests/test_lib.py new file mode 100644 index 0000000..e209d16 --- /dev/null +++ b/tests/test_lib.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dtos import DTOConfig, register_config +from dtos.internals.config_registry import global_config_registry + + +def test_register_global_config_on_object() -> None: + class Base: + pass + + class Derived(Base): + pass + + config = DTOConfig() + + register_config(object, config) + + assert global_config_registry.get(Base) == config + assert global_config_registry.get(Derived) == config diff --git a/tests/test_src.py b/tests/test_src.py deleted file mode 100644 index 2e641f8..0000000 --- a/tests/test_src.py +++ /dev/null @@ -1,5 +0,0 @@ -from dtos import return_three - - -def test_return_three() -> None: - assert return_three() == 3 diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..9d48db4 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/typesafety/test_interface.yml b/typesafety/test_interface.yml new file mode 100644 index 0000000..9b83f46 --- /dev/null +++ b/typesafety/test_interface.yml @@ -0,0 +1,98 @@ +- case: test_create_dto_from_model + main: | + import msgspec + + from dtos import create_dto + + class Model(msgspec.Struct): + a: int + + reveal_type(create_dto(Model)) + out: | + main:8: note: Revealed type is "dtos.dto.DTO[main.Model]" + +- case: test_create_dto_from_collection + main: | + import msgspec + + from dtos import create_dto + + class Model(msgspec.Struct): + a: int + + reveal_type(create_dto(list[Model])) + out: | + main:8: note: Revealed type is "dtos.dto.DTO[builtins.list[main.Model]]" + +- case: test_create_dto_from_complex_collection_builtins + main: | + from typing import Union + + from dtos import create_dto + + reveal_type(create_dto(dict[str, Union[str, int]])) + out: | + main:5: note: Revealed type is "dtos.dto.DTO[builtins.dict[builtins.str, Union[builtins.str, builtins.int]]]" + +- case: test_create_dto_from_union_of_builtins + main: | + from typing import Union + + from dtos import create_dto + + reveal_type(create_dto(Union[int, str])) + out: | + main:4: note: Revealed type is "dtos.dto.DTO[Union[builtins.int, builtins.str]]" + expect_fail: True + +- case: test_create_dto_from_union_of_dataclasses + main: | + from dataclasses import dataclass + from typing import Union + + from dtos import create_dto + + @dataclass + class Model1: ... + + @dataclass + class Model2: ... + + reveal_type(create_dto(Union[Model1, Model2])) + out: | + main:12: note: Revealed type is "dtos.dto.DTO[Union[main.Model1, main.Model2]]" + expect_fail: True + +- case: test_create_dto_from_mapping_of_dataclasses + main: | + from dataclasses import dataclass + from typing import Union + + from dtos import create_dto + + @dataclass + class Model1: ... + + @dataclass + class Model2: ... + + reveal_type(create_dto(dict[str, Union[Model1, Model2]])) + out: | + main:12: note: Revealed type is "dtos.dto.DTO[builtins.dict[builtins.str, Union[main.Model1, main.Model2]]]" + +- case: test_create_dto_from_list_of_dataclass_union + main: | + from dataclasses import dataclass + from typing import Union + + from dtos import create_dto + + @dataclass + class Model1: ... + + @dataclass + class Model2: ... + + reveal_type(create_dto(list[Union[Model1, Model2]])) + out: | + main:12: note: Revealed type is "dtos.dto.DTO[builtins.list[Union[main.Model1, main.Model2]]]"