diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..aaa9f23f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + # ignore all test cases in tests/ + tests/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0cb716c..a9b77278 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,9 @@ jobs: files.pythonhosted.org install.python-poetry.org pypi.org + *.atproto.blue + *.marshal.dev + plc.directory - name: Checkout repository. uses: actions/checkout@v4 diff --git a/CHANGES.md b/CHANGES.md index 01edded4..e11bdf00 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,11 +4,8 @@ **21.12.2023** -# ❗Breaking changes +**❗Breaking changes:** SDK was split into many packages. This affects imports in your codebase. [Read more](https://atproto.blue/en/latest/readme.content.html#sdk-structure) -SDK was split into many packages. This affects imports in your codebase. [Read more](https://atproto.blue/en/latest/readme.content.html#sdk-structure) - -## What's Changed * New SDK structure by @MarshalX in https://github.com/MarshalX/atproto/pull/214 and https://github.com/MarshalX/atproto/pull/216 * Fix decoding of CAR root by @MarshalX in https://github.com/MarshalX/atproto/pull/213 * Fix parsing of BlobRef in CBOR by @MarshalX in https://github.com/MarshalX/atproto/pull/215 diff --git a/README.md b/README.md index 3e56bae0..0e2e2ef2 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ if __name__ == '__main__': 🍿 [Example project with custom feed generator](https://github.com/MarshalX/bluesky-feed-generator) -πŸ”₯ [Firehose data streaming is available!](https://atproto.blue/en/latest/firehose.html) +πŸ”₯ [Firehose data streaming is available](https://atproto.blue/en/latest/firehose.html) + +🌐 [Identity resolvers for DID and Handle](https://atproto.blue/en/latest/atproto_identity/identity.html) ### Introduction @@ -138,13 +140,17 @@ Useful links to continue: ### SDK structure The SDK is built upon the following components: -- `atproto` β€” the package that contains import shortcuts to other packages. -- `atproto_cli` β€” the package that contains the CLI tool to generate code. -- `atproto_client` β€” the package that contains the XRPC Client, data models, and utils like rich text helper. -- `atproto_codegen` β€” the package that contains the code generator of models, clients, and namespaces. -- `atproto_core` β€” the package that contains the core of the SDK. Base class of exceptions, tools to work with NSID, AT URI Schemes, CID, and CAR files. -- `atproto_firehose` β€” the package that contains the Firehose (data streaming) client and models. -- `atproto_lexicon` β€” the package that contains the lexicon parser. + +| Package | Description | +|--------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `atproto` | The package that contains import shortcuts to other packages. | +| `atproto_cli` | The package that contains the CLI tool to generate code. | +| `atproto_client` | The package that contains the XRPC Client, data models, and utils like rich text helper. | +| `atproto_codegen` | The package that contains the code generator of models, clients, and namespaces. | +| `atproto_core` | The package that contains the core of the SDK. Base class of exceptions, tools to work with NSID, AT URI Schemes, CID, and CAR files. | +| `atproto_firehose` | The package that contains the Firehose (data streaming) client and models. | +| `atproto_identity` | The package that contains the identity resolvers for DID and Handle. | +| `atproto_lexicon` | The package that contains the lexicon parser. | I highly recommend you to use the `atproto` package to import everything that you need. It contains shortcuts to all other packages. diff --git a/docs/source/atproto/atproto_core.did_doc.did_doc.rst b/docs/source/atproto/atproto_core.did_doc.did_doc.rst new file mode 100644 index 00000000..46fa9250 --- /dev/null +++ b/docs/source/atproto/atproto_core.did_doc.did_doc.rst @@ -0,0 +1,7 @@ +atproto\_core.did\_doc.did\_doc +=============================== + +.. automodule:: atproto_core.did_doc.did_doc + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_core.did_doc.rst b/docs/source/atproto/atproto_core.did_doc.rst new file mode 100644 index 00000000..2c0f367a --- /dev/null +++ b/docs/source/atproto/atproto_core.did_doc.rst @@ -0,0 +1,15 @@ +atproto\_core.did\_doc +====================== + +.. automodule:: atproto_core.did_doc + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + atproto_core.did_doc.did_doc diff --git a/docs/source/atproto/atproto_core.rst b/docs/source/atproto/atproto_core.rst index 2e622816..d98af432 100644 --- a/docs/source/atproto/atproto_core.rst +++ b/docs/source/atproto/atproto_core.rst @@ -15,6 +15,7 @@ Subpackages atproto_core.car atproto_core.cbor atproto_core.cid + atproto_core.did_doc atproto_core.nsid atproto_core.uri diff --git a/docs/source/atproto/atproto_identity.cache.base_cache.rst b/docs/source/atproto/atproto_identity.cache.base_cache.rst new file mode 100644 index 00000000..b1fc73ac --- /dev/null +++ b/docs/source/atproto/atproto_identity.cache.base_cache.rst @@ -0,0 +1,7 @@ +atproto\_identity.cache.base\_cache +=================================== + +.. automodule:: atproto_identity.cache.base_cache + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.cache.in_memory_cache.rst b/docs/source/atproto/atproto_identity.cache.in_memory_cache.rst new file mode 100644 index 00000000..c146e6be --- /dev/null +++ b/docs/source/atproto/atproto_identity.cache.in_memory_cache.rst @@ -0,0 +1,7 @@ +atproto\_identity.cache.in\_memory\_cache +========================================= + +.. automodule:: atproto_identity.cache.in_memory_cache + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.cache.models.rst b/docs/source/atproto/atproto_identity.cache.models.rst new file mode 100644 index 00000000..774c689f --- /dev/null +++ b/docs/source/atproto/atproto_identity.cache.models.rst @@ -0,0 +1,7 @@ +atproto\_identity.cache.models +============================== + +.. automodule:: atproto_identity.cache.models + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.cache.rst b/docs/source/atproto/atproto_identity.cache.rst new file mode 100644 index 00000000..0d0de45b --- /dev/null +++ b/docs/source/atproto/atproto_identity.cache.rst @@ -0,0 +1,17 @@ +atproto\_identity.cache +======================= + +.. automodule:: atproto_identity.cache + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.cache.base_cache + atproto_identity.cache.in_memory_cache + atproto_identity.cache.models diff --git a/docs/source/atproto/atproto_identity.did.atproto_data.rst b/docs/source/atproto/atproto_identity.did.atproto_data.rst new file mode 100644 index 00000000..59d1a6e4 --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.atproto_data.rst @@ -0,0 +1,7 @@ +atproto\_identity.did.atproto\_data +=================================== + +.. automodule:: atproto_identity.did.atproto_data + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.did.models.rst b/docs/source/atproto/atproto_identity.did.models.rst new file mode 100644 index 00000000..5ab75ad9 --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.models.rst @@ -0,0 +1,7 @@ +atproto\_identity.did.models +============================ + +.. automodule:: atproto_identity.did.models + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.did.resolver.rst b/docs/source/atproto/atproto_identity.did.resolver.rst new file mode 100644 index 00000000..cc5594f4 --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.resolver.rst @@ -0,0 +1,7 @@ +atproto\_identity.did.resolver +============================== + +.. automodule:: atproto_identity.did.resolver + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.did.resolvers.base_resolver.rst b/docs/source/atproto/atproto_identity.did.resolvers.base_resolver.rst new file mode 100644 index 00000000..00cd507a --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.resolvers.base_resolver.rst @@ -0,0 +1,7 @@ +atproto\_identity.did.resolvers.base\_resolver +============================================== + +.. automodule:: atproto_identity.did.resolvers.base_resolver + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.did.resolvers.plc_resolver.rst b/docs/source/atproto/atproto_identity.did.resolvers.plc_resolver.rst new file mode 100644 index 00000000..fc164b8a --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.resolvers.plc_resolver.rst @@ -0,0 +1,7 @@ +atproto\_identity.did.resolvers.plc\_resolver +============================================= + +.. automodule:: atproto_identity.did.resolvers.plc_resolver + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.did.resolvers.rst b/docs/source/atproto/atproto_identity.did.resolvers.rst new file mode 100644 index 00000000..301f7a35 --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.resolvers.rst @@ -0,0 +1,17 @@ +atproto\_identity.did.resolvers +=============================== + +.. automodule:: atproto_identity.did.resolvers + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.did.resolvers.base_resolver + atproto_identity.did.resolvers.plc_resolver + atproto_identity.did.resolvers.web_resolver diff --git a/docs/source/atproto/atproto_identity.did.resolvers.web_resolver.rst b/docs/source/atproto/atproto_identity.did.resolvers.web_resolver.rst new file mode 100644 index 00000000..7115d3dc --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.resolvers.web_resolver.rst @@ -0,0 +1,7 @@ +atproto\_identity.did.resolvers.web\_resolver +============================================= + +.. automodule:: atproto_identity.did.resolvers.web_resolver + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.did.rst b/docs/source/atproto/atproto_identity.did.rst new file mode 100644 index 00000000..e359ec88 --- /dev/null +++ b/docs/source/atproto/atproto_identity.did.rst @@ -0,0 +1,25 @@ +atproto\_identity.did +===================== + +.. automodule:: atproto_identity.did + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.did.resolvers + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.did.atproto_data + atproto_identity.did.models + atproto_identity.did.resolver diff --git a/docs/source/atproto/atproto_identity.exceptions.rst b/docs/source/atproto/atproto_identity.exceptions.rst new file mode 100644 index 00000000..d08eec8d --- /dev/null +++ b/docs/source/atproto/atproto_identity.exceptions.rst @@ -0,0 +1,7 @@ +atproto\_identity.exceptions +============================ + +.. automodule:: atproto_identity.exceptions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.handle.resolver.rst b/docs/source/atproto/atproto_identity.handle.resolver.rst new file mode 100644 index 00000000..dfc87839 --- /dev/null +++ b/docs/source/atproto/atproto_identity.handle.resolver.rst @@ -0,0 +1,7 @@ +atproto\_identity.handle.resolver +================================= + +.. automodule:: atproto_identity.handle.resolver + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.handle.rst b/docs/source/atproto/atproto_identity.handle.rst new file mode 100644 index 00000000..dd765aba --- /dev/null +++ b/docs/source/atproto/atproto_identity.handle.rst @@ -0,0 +1,15 @@ +atproto\_identity.handle +======================== + +.. automodule:: atproto_identity.handle + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.handle.resolver diff --git a/docs/source/atproto/atproto_identity.resolver.rst b/docs/source/atproto/atproto_identity.resolver.rst new file mode 100644 index 00000000..c5f444a3 --- /dev/null +++ b/docs/source/atproto/atproto_identity.resolver.rst @@ -0,0 +1,7 @@ +atproto\_identity.resolver +========================== + +.. automodule:: atproto_identity.resolver + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/atproto/atproto_identity.rst b/docs/source/atproto/atproto_identity.rst new file mode 100644 index 00000000..08bed1a8 --- /dev/null +++ b/docs/source/atproto/atproto_identity.rst @@ -0,0 +1,26 @@ +atproto\_identity +================= + +.. automodule:: atproto_identity + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.cache + atproto_identity.did + atproto_identity.handle + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + atproto_identity.exceptions + atproto_identity.resolver diff --git a/docs/source/atproto/modules.rst b/docs/source/atproto/modules.rst index 3af87386..1cb5a2e1 100644 --- a/docs/source/atproto/modules.rst +++ b/docs/source/atproto/modules.rst @@ -10,5 +10,6 @@ packages atproto_codegen atproto_core atproto_firehose + atproto_identity atproto_lexicon atproto_server diff --git a/docs/source/atproto_identity/cache.rst b/docs/source/atproto_identity/cache.rst new file mode 100644 index 00000000..89bce5e3 --- /dev/null +++ b/docs/source/atproto_identity/cache.rst @@ -0,0 +1,38 @@ +Cache +===== + +Cache could be used to store previously resolved DID Documents. SDK provides ``DidInMemoryCache`` and ``DidBaseCache`` classes. + +``DidInMemoryCache`` is a simple implementation of ``DidBaseCache`` that stores data in memory. Feel free to use it as real cache or as a reference implementation. + +``DidBaseCache`` is an abstract class that could be used to implement custom cache. Please note that there is 2 base classes. One for synchronous and another for asynchronous cache. + +Here is an example of how to use ``DidInMemoryCache`` with ``IdResolver``: + +.. code-block:: python + + from atproto import DidInMemoryCache, IdResolver # for async AsyncDidInMemoryCache and AsyncIdResolver + + cache = DidInMemoryCache() + resolver = IdResolver(cache=cache) + did_doc = resolver.did.resolve('did:web:feed.atproto.blue') + + # Now did_document is cached and could be retrieved without network request + did_doc = resolver.did.resolve('did:web:feed.atproto.blue') + + # Clear cache + cache.clear() + + # Now did_document is not cached and will be retrieved with network request + did_doc = resolver.did.resolve('did:web:feed.atproto.blue') + + +.. automodule:: atproto_identity.cache.in_memory_cache + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: atproto_identity.cache.base_cache + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/atproto_identity/did_resolver.rst b/docs/source/atproto_identity/did_resolver.rst new file mode 100644 index 00000000..dbf734ed --- /dev/null +++ b/docs/source/atproto_identity/did_resolver.rst @@ -0,0 +1,7 @@ +DID Resolver +============ + +.. automodule:: atproto_identity.did.resolver + :members: + :undoc-members: + :inherited-members: diff --git a/docs/source/atproto_identity/handle_resolver.rst b/docs/source/atproto_identity/handle_resolver.rst new file mode 100644 index 00000000..0be5fb96 --- /dev/null +++ b/docs/source/atproto_identity/handle_resolver.rst @@ -0,0 +1,7 @@ +Handle Resolver +=============== + +.. automodule:: atproto_identity.handle.resolver + :members: + :undoc-members: + :inherited-members: diff --git a/docs/source/atproto_identity/id_resolver.rst b/docs/source/atproto_identity/id_resolver.rst new file mode 100644 index 00000000..9a45c398 --- /dev/null +++ b/docs/source/atproto_identity/id_resolver.rst @@ -0,0 +1,7 @@ +ID Resolver +=========== + +.. automodule:: atproto_identity.resolver + :members: + :undoc-members: + :inherited-members: diff --git a/docs/source/atproto_identity/identity.rst b/docs/source/atproto_identity/identity.rst new file mode 100644 index 00000000..0927dc6c --- /dev/null +++ b/docs/source/atproto_identity/identity.rst @@ -0,0 +1,39 @@ +Identity (DID and Handle resolvers) +=================================== + +Check out what is a DID Document in the :doc:`../did_doc`. + +AT Protocol uses two identifiers: DID and Handle. Handles are DNS names while DIDs are an emerging W3C standard which act as secure & stable IDs. + +The AT Protocol Identity module provides a way to resolve DIDs and Handles. It also provides a way to cache the results of these resolutions. + +Under the hood, the Identity module resolves Handlers using DNS and HTTP. It resolves DIDs using PLC directory and HTTP. + +Typically, you don't need to care about the details of how the Identity module works. You can simply use the ``IdResolver`` class to resolve DIDs and Handles: + +.. code-block:: python + + from atproto import IdResolver # for async use AsyncIdResolver + + resolver = IdResolver() + did = resolver.handle.resolve('test.marshal.dev') + did_doc = resolver.did.resolve(did) + + print(did) + print(did_doc) + + +Learn how to use cache to speed up the resolution process in the :doc:`cache` section. + +.. automodule:: atproto_identity.resolver + :members: + :undoc-members: + :inherited-members: + +.. toctree:: + :maxdepth: 4 + + id_resolver + handle_resolver + did_resolver + cache diff --git a/docs/source/conf.py b/docs/source/conf.py index 702359fd..7b46ffa5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,6 +42,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', + 'sphinx.ext.autosectionlabel', 'sphinxext.opengraph', 'sphinx_copybutton', 'sphinx_favicon', @@ -129,6 +130,9 @@ autodoc_pydantic_model_show_config_summary = False +autosectionlabel_prefix_document = True + + def setup(app: 'Sphinx') -> None: from docs.source.alias_resolver import resolve_internal_aliases, resolve_intersphinx_aliases diff --git a/docs/source/did_doc.rst b/docs/source/did_doc.rst new file mode 100644 index 00000000..e4232532 --- /dev/null +++ b/docs/source/did_doc.rst @@ -0,0 +1,30 @@ +DID Document +============ + +Check out how to resolve a DID Document in :doc:`atproto_identity/identity`. + +After a DID document has been resolved, atproto-specific information needs to be extracted. This parsing process is agnostic to the DID method used to resolve the document. + +SDK automatically parses the DID document and provides a DID document object after resolving. + +If you got a DID document from other sources, you can also parse it: + +.. code-block:: python + + from atproto import Client, DidDocument + + client = Client() + client.login('username', 'password') + + response = client.com.atproto.repo.describe_repo({'repo': 'did:plc:kvwvcn5iqfooopmyzvb4qzba'}) + did_doc = DidDocument.from_dict(response.did_doc) + print(did_doc.get_pds_endpoint()) + print(did_doc.get_handle()) + + +Read more about DID document in official documentation: https://atproto.com/specs/did + +.. automodule:: atproto_core.did_doc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/exceptions.rst b/docs/source/exceptions.rst index e555a987..c18f03df 100644 --- a/docs/source/exceptions.rst +++ b/docs/source/exceptions.rst @@ -28,6 +28,15 @@ Firehose :show-inheritance: +Identity +######## + +.. automodule:: atproto_identity.exceptions + :members: + :undoc-members: + :show-inheritance: + + Lexicon ####### diff --git a/docs/source/index.rst b/docs/source/index.rst index 1372fc49..10c25797 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,9 +22,10 @@ Documentation atproto_client/clients namespace models + atproto_identity/identity .. toctree:: - :caption: Utils + :caption: Core :maxdepth: 4 nsid @@ -32,6 +33,12 @@ Documentation uri car cbor + did_doc + +.. toctree:: + :caption: Utils + :maxdepth: 4 + text_builder .. toctree:: diff --git a/docs/source/readme.content.md b/docs/source/readme.content.md index a014fdcd..6bf94c6b 100644 --- a/docs/source/readme.content.md +++ b/docs/source/readme.content.md @@ -64,13 +64,17 @@ Useful links to continue: ### SDK structure The SDK is built upon the following components: -- `atproto` β€” the package that contains import shortcuts to other packages. -- `atproto_cli` β€” the package that contains the CLI tool to generate code. -- `atproto_client` β€” the package that contains the XRPC Client, data models, and utils like rich text helper. -- `atproto_codegen` β€” the package that contains the code generator of models, clients, and namespaces. -- `atproto_core` β€” the package that contains the core of the SDK. Base class of exceptions, tools to work with NSID, AT URI Schemes, CID, and CAR files. -- `atproto_firehose` β€” the package that contains the Firehose (data streaming) client and models. -- `atproto_lexicon` β€” the package that contains the lexicon parser. + +| Package | Description | +|--------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `atproto` | The package that contains import shortcuts to other packages. | +| `atproto_cli` | The package that contains the CLI tool to generate code. | +| `atproto_client` | The package that contains the XRPC Client, data models, and utils like rich text helper. | +| `atproto_codegen` | The package that contains the code generator of models, clients, and namespaces. | +| `atproto_core` | The package that contains the core of the SDK. Base class of exceptions, tools to work with NSID, AT URI Schemes, CID, and CAR files. | +| `atproto_firehose` | The package that contains the Firehose (data streaming) client and models. | +| `atproto_identity` | The package that contains the identity resolvers for DID and Handle. | +| `atproto_lexicon` | The package that contains the lexicon parser. | I highly recommend you to use the `atproto` package to import everything that you need. It contains shortcuts to all other packages. diff --git a/packages/atproto/__init__.py b/packages/atproto/__init__.py index 16383b55..a23926d2 100644 --- a/packages/atproto/__init__.py +++ b/packages/atproto/__init__.py @@ -2,6 +2,7 @@ from atproto_client import utils as client_utils from atproto_core.car import CAR from atproto_core.cid import CID, CIDType +from atproto_core.did_doc import DidDocument from atproto_core.nsid import NSID from atproto_core.uri import AtUri from atproto_firehose import ( @@ -13,6 +14,8 @@ parse_subscribe_repos_message, ) from atproto_firehose import models as firehose_models +from atproto_identity.cache.in_memory_cache import AsyncDidInMemoryCache, DidInMemoryCache +from atproto_identity.resolver import AsyncIdResolver, IdResolver __all__ = [ 'AsyncClient', @@ -22,6 +25,7 @@ 'CAR', 'CID', 'CIDType', + 'DidDocument', 'NSID', 'AtUri', 'AsyncFirehoseSubscribeLabelsClient', @@ -31,4 +35,8 @@ 'parse_subscribe_labels_message', 'parse_subscribe_repos_message', 'firehose_models', + 'AsyncDidInMemoryCache', + 'DidInMemoryCache', + 'AsyncIdResolver', + 'IdResolver', ] diff --git a/packages/atproto/exceptions.py b/packages/atproto/exceptions.py index cb0c5681..d5deb7af 100644 --- a/packages/atproto/exceptions.py +++ b/packages/atproto/exceptions.py @@ -1,4 +1,5 @@ from atproto_client.exceptions import * from atproto_core.exceptions import * from atproto_firehose.exceptions import * +from atproto_identity.exceptions import * from atproto_lexicon.exceptions import * diff --git a/packages/atproto_core/did_doc/__init__.py b/packages/atproto_core/did_doc/__init__.py new file mode 100644 index 00000000..5cc4eb6e --- /dev/null +++ b/packages/atproto_core/did_doc/__init__.py @@ -0,0 +1,3 @@ +from .did_doc import DidDocument, Service, SigningKey, VerificationMethod, is_valid_did_doc, parse_did_doc + +__all__ = ['DidDocument', 'Service', 'SigningKey', 'VerificationMethod', 'is_valid_did_doc', 'parse_did_doc'] diff --git a/packages/atproto_core/did_doc/did_doc.py b/packages/atproto_core/did_doc/did_doc.py new file mode 100644 index 00000000..b6191309 --- /dev/null +++ b/packages/atproto_core/did_doc/did_doc.py @@ -0,0 +1,195 @@ +import typing as t +from dataclasses import dataclass +from urllib.parse import urlparse + +from pydantic import BaseModel, Field, ValidationError + +_AT_URI_PREFIX = 'at://' +_AT_URI_PREFIX_LEN = len(_AT_URI_PREFIX) +_ATPROTO_KEY_ID = '#atproto' + + +@dataclass +class SigningKey: + """Public signing key for the account.""" + + type: str + public_key_multibase: str + + +def get_did(did_doc: 'DidDocument') -> str: + """Returns the DID of the given DID document. + + Returns: + :obj:`str`: The DID of the given DID document. + """ + return did_doc.id + + +def get_handle(did_doc: 'DidDocument') -> t.Optional[str]: + """Returns the handle of the given DID document. + + Returns: + :obj:`str`: The handle of the given DID document, or ``None`` if not found. + """ + aka = did_doc.also_known_as + if not aka: + return None + + for name in aka: + if name.startswith(_AT_URI_PREFIX): + return name[_AT_URI_PREFIX_LEN:] + + return None + + +def get_signing_key(did_doc: 'DidDocument') -> t.Optional['SigningKey']: + """Returns the signing key of the given DID document. + + Returns: + :obj:`SigningKey`: The signing key of the given DID document, or ``None`` if not found. + """ + did = get_did(did_doc) + + keys = did_doc.verification_method + if not keys: + return None + + for key in keys: + if (key.id == _ATPROTO_KEY_ID or key.id == f'{did}{_ATPROTO_KEY_ID}') and key.public_key_multibase: + return SigningKey(type=key.type, public_key_multibase=key.public_key_multibase) + + return None + + +def _validate_url(url: str) -> t.Optional[str]: + try: + parsed_url = urlparse(url) + except Exception: # noqa: BLE001 + return None + + if parsed_url.scheme not in {'http', 'https'}: + return None + if parsed_url.hostname is None: + return None + + return url + + +def get_service_endpoint(did_doc: 'DidDocument', id_: str, type_: str) -> t.Optional[str]: + """Returns the service endpoint of the given DID document. + + Args: + id_: The service ID. + type_: The service type. + + Returns: + :obj:`str`: The service endpoint of the given DID document, or ``None`` if not found. + """ + did = get_did(did_doc) + + services = did_doc.service + if not services: + return None + + for service in services: + if (service.id == id_ or service.id == f'{did}{id_}') and service.type == type_: + return _validate_url(service.service_endpoint) + + return None + + +def get_pds_endpoint(did_doc: 'DidDocument') -> t.Optional[str]: + """Returns the personal data server endpoint of the given DID document. + + Returns: + :obj:`str`: The personal data server endpoint of the given DID document, or ``None`` if not found. + """ + return get_service_endpoint(did_doc, '#atproto_pds', 'AtprotoPersonalDataServer') + + +def get_feed_gen_endpoint(did_doc: 'DidDocument') -> t.Optional[str]: + """Returns the feed generator endpoint of the given DID document. + + Returns: + :obj:`str`: The feed generator endpoint of the given DID document, or ``None`` if not found. + """ + return get_service_endpoint(did_doc, '#bsky_fg', 'BskyFeedGenerator') + + +def get_notif_endpoint(did_doc: 'DidDocument') -> t.Optional[str]: + """Returns the notification endpoint of the given DID document. + + Returns: + :obj:`str`: The notification endpoint of the given DID document, or ``None`` if not found. + """ + return get_service_endpoint(did_doc, '#bsky_notif', 'BskyNotificationService') + + +def is_valid_did_doc(did_doc: t.Union[dict, t.Any]) -> bool: + """Returns whether the given DID document is valid. + + Args: + did_doc: The raw DID document. + + Returns: + :obj:`bool`: Whether the given DID document is valid. + """ + try: + parse_did_doc(did_doc) + return True + except ValidationError: + return False + + +def parse_did_doc(did_doc: t.Union[dict, t.Any]) -> 'DidDocument': + """Parses a DID document. + + Args: + did_doc: The raw DID document. + + Returns: + :obj:`DidDocument`: The parsed DID document. + """ + if hasattr(did_doc, 'to_dict'): + return DidDocument(**did_doc.to_dict()) + + return DidDocument(**did_doc) + + +class VerificationMethod(BaseModel): + """Verification method.""" + + id: str + type: str + controller: str + public_key_multibase: t.Optional[str] = Field(default=None, alias='publicKeyMultibase') + + +class Service(BaseModel): + """Service.""" + + id: str + type: str + service_endpoint: t.Union[str, dict] = Field(alias='serviceEndpoint') + + +class DidDocument(BaseModel): + """DID document.""" + + id: str + also_known_as: t.Optional[t.List[str]] = Field(default=None, alias='alsoKnownAs') + verification_method: t.Optional[t.List['VerificationMethod']] = Field(default=None, alias='verificationMethod') + service: t.Optional[t.List['Service']] = None + + get_signing_key = get_signing_key + get_handle = get_handle + get_service_endpoint = get_service_endpoint + get_pds_endpoint = get_pds_endpoint + get_feed_gen_endpoint = get_feed_gen_endpoint + get_notif_endpoint = get_notif_endpoint + + @classmethod + def from_dict(cls, did_doc: t.Union[dict, t.Any]) -> 'DidDocument': + """Parses a DID document.""" + return parse_did_doc(did_doc) diff --git a/tests/models/__init__.py b/packages/atproto_identity/__init__.py similarity index 100% rename from tests/models/__init__.py rename to packages/atproto_identity/__init__.py diff --git a/tests/models/tests/__init__.py b/packages/atproto_identity/cache/__init__.py similarity index 100% rename from tests/models/tests/__init__.py rename to packages/atproto_identity/cache/__init__.py diff --git a/packages/atproto_identity/cache/base_cache.py b/packages/atproto_identity/cache/base_cache.py new file mode 100644 index 00000000..4ffa2746 --- /dev/null +++ b/packages/atproto_identity/cache/base_cache.py @@ -0,0 +1,145 @@ +import typing as t +from abc import ABC, abstractmethod + +if t.TYPE_CHECKING: + from atproto_core.did_doc import DidDocument + + from atproto_identity.cache.models import CachedDidResult + + +_DEFAULT_STALE_TTL = 60 * 60 # 1 hour +_DEFAULT_MAX_TTL = 60 * 60 * 24 # 1 day + + +GetDocCallback = t.Callable[[], t.Optional['DidDocument']] +AsyncGetDocCallback = t.Callable[[], t.Coroutine[t.Any, t.Any, t.Optional['DidDocument']]] + + +class _DidBaseCache: + def __init__(self, stale_ttl: t.Optional[int] = None, max_ttl: t.Optional[int] = None) -> None: + self.stale_ttl = stale_ttl or _DEFAULT_STALE_TTL + self.max_ttl = max_ttl or _DEFAULT_MAX_TTL + + +class DidBaseCache(_DidBaseCache, ABC): + """Abstract DID Cache. + + Args: + stale_ttl: Stale TTL in seconds. Default is 1 hour. + max_ttl: Max TTL in seconds. Default is 1 day. + """ + + def __init__(self, stale_ttl: t.Optional[int] = None, max_ttl: t.Optional[int] = None) -> None: + super().__init__(stale_ttl, max_ttl) + + @abstractmethod + def get(self, did: str) -> t.Optional['CachedDidResult']: + """Get cached DID. + + Args: + did: DID. + + Returns: + :obj:`CachedDidResult`: Cached DID result or ``None`` if not found. + """ + raise NotImplementedError + + @abstractmethod + def set(self, did: str, document: 'DidDocument') -> None: + """Set cached DID. + + Args: + did: DID. + document: DID document. + """ + raise NotImplementedError + + @abstractmethod + def refresh(self, did: str, get_doc_callback: GetDocCallback) -> None: + """Refresh cached DID. + + Args: + did: DID. + get_doc_callback: Get DID document callback. + """ + raise NotImplementedError + + @abstractmethod + def delete(self, did: str) -> None: + """Delete cached DID. + + Args: + did: DID. + """ + raise NotImplementedError + + @abstractmethod + def clear(self) -> None: + """Clear cached DIDs. + + Note: + This method is used to clear all cached DIDs. + """ + raise NotImplementedError + + +class AsyncDidBaseCache(_DidBaseCache, ABC): + """Asynchronous Abstract DID Cache. + + Args: + stale_ttl: Stale TTL in seconds. Default is 1 hour. + max_ttl: Max TTL in seconds. Default is 1 day. + """ + + def __init__(self, stale_ttl: t.Optional[int] = None, max_ttl: t.Optional[int] = None) -> None: + super().__init__(stale_ttl, max_ttl) + + @abstractmethod + async def get(self, did: str) -> t.Optional['CachedDidResult']: + """Get cached DID. + + Args: + did: DID. + + Returns: + :obj:`CachedDidResult`: Cached DID result or ``None`` if not found. + """ + raise NotImplementedError + + @abstractmethod + async def set(self, did: str, document: 'DidDocument') -> None: + """Set cached DID. + + Args: + did: DID. + document: DID document. + """ + raise NotImplementedError + + @abstractmethod + async def refresh(self, did: str, get_doc_callback: AsyncGetDocCallback) -> None: + """Refresh cached DID. + + Args: + did: DID. + get_doc_callback: Get DID document callback. + """ + raise NotImplementedError + + @abstractmethod + async def delete(self, did: str) -> None: + """Delete cached DID. + + Args: + did: DID. + """ + raise NotImplementedError + + @abstractmethod + async def clear(self) -> None: + """Clear cached DIDs. + + Note: + This method is used to clear all cached DIDs. + """ + raise NotImplementedError diff --git a/packages/atproto_identity/cache/in_memory_cache.py b/packages/atproto_identity/cache/in_memory_cache.py new file mode 100644 index 00000000..1563887c --- /dev/null +++ b/packages/atproto_identity/cache/in_memory_cache.py @@ -0,0 +1,70 @@ +import typing as t +from datetime import datetime, timezone + +from atproto_core.did_doc import DidDocument + +from atproto_identity.cache.base_cache import AsyncDidBaseCache, DidBaseCache +from atproto_identity.cache.models import CachedDid, CachedDidResult + +if t.TYPE_CHECKING: + from atproto_identity.cache.base_cache import AsyncGetDocCallback, GetDocCallback + + +def _datetime_now() -> datetime: + return datetime.now(timezone.utc) + + +class DidInMemoryCache(DidBaseCache): + def __init__(self, *args, **kwargs: t.Any) -> None: + super().__init__(*args, **kwargs) + + self._cache: t.Dict[str, CachedDid] = {} + + def set(self, did: str, document: DidDocument) -> None: + self._cache[did] = CachedDid(document, _datetime_now()) + + def refresh(self, did: str, get_doc_callback: 'GetDocCallback') -> None: + doc = get_doc_callback() + if doc: + self.set(did, doc) + + def delete(self, did: str) -> None: + del self._cache[did] + + def clear(self) -> None: + self._cache.clear() + + def get(self, did: str) -> t.Optional[CachedDidResult]: + val = self._cache.get(did) + if not val: + return None + + now = _datetime_now().timestamp() + expired = now > val.updated_at.timestamp() + self.max_ttl + stale = now > val.updated_at.timestamp() + self.stale_ttl + + return CachedDidResult(did, val.document, val.updated_at, stale, expired) + + +class AsyncDidInMemoryCache(AsyncDidBaseCache): + def __init__(self, *args, **kwargs: t.Any) -> None: + super().__init__(*args, **kwargs) + + self._in_memory_cache = DidInMemoryCache(*args, **kwargs) + + async def set(self, did: str, document: DidDocument) -> None: + self._in_memory_cache.set(did, document) + + async def refresh(self, did: str, get_doc_callback: 'AsyncGetDocCallback') -> None: + doc = await get_doc_callback() + if doc: + await self.set(did, doc) + + async def delete(self, did: str) -> None: + self._in_memory_cache.delete(did) + + async def clear(self) -> None: + self._in_memory_cache.clear() + + async def get(self, did: str) -> t.Optional[CachedDidResult]: + return self._in_memory_cache.get(did) diff --git a/packages/atproto_identity/cache/models.py b/packages/atproto_identity/cache/models.py new file mode 100644 index 00000000..f7ccbd73 --- /dev/null +++ b/packages/atproto_identity/cache/models.py @@ -0,0 +1,25 @@ +import typing as t +from dataclasses import dataclass +from datetime import datetime + +if t.TYPE_CHECKING: + from atproto_core.did_doc import DidDocument + + +@dataclass +class CachedDid: + """Cached DID.""" + + document: 'DidDocument' + updated_at: datetime + + +@dataclass +class CachedDidResult: + """Cached DID result.""" + + did: str + document: 'DidDocument' + updated_at: datetime + stale: bool + expired: bool diff --git a/packages/atproto_identity/did/__init__.py b/packages/atproto_identity/did/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/atproto_identity/did/atproto_data.py b/packages/atproto_identity/did/atproto_data.py new file mode 100644 index 00000000..56e5091b --- /dev/null +++ b/packages/atproto_identity/did/atproto_data.py @@ -0,0 +1,14 @@ +import typing as t + +from atproto_identity.did.models import AtprotoData + +if t.TYPE_CHECKING: + from atproto_core.did_doc import DidDocument + + +def ensure_atproto_document(_: 'DidDocument') -> AtprotoData: + raise NotImplementedError + + +def ensure_atproto_key(_: 'DidDocument') -> str: + raise NotImplementedError diff --git a/packages/atproto_identity/did/models.py b/packages/atproto_identity/did/models.py new file mode 100644 index 00000000..e754116a --- /dev/null +++ b/packages/atproto_identity/did/models.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class AtprotoData: + did: str + signing_key: str + handle: str + pds: str diff --git a/packages/atproto_identity/did/resolver.py b/packages/atproto_identity/did/resolver.py new file mode 100644 index 00000000..3861aae2 --- /dev/null +++ b/packages/atproto_identity/did/resolver.py @@ -0,0 +1,116 @@ +import typing as t + +from atproto_identity.did.resolvers.base_resolver import AsyncBaseResolver, BaseResolver +from atproto_identity.did.resolvers.plc_resolver import AsyncDidPlcResolver, DidPlcResolver +from atproto_identity.did.resolvers.web_resolver import AsyncDidWebResolver, DidWebResolver +from atproto_identity.exceptions import PoorlyFormattedDidError, UnsupportedDidMethodError + +if t.TYPE_CHECKING: + from atproto_identity.cache.base_cache import AsyncDidBaseCache, DidBaseCache + + +_BASE_PLC_URL = 'https://plc.directory' +_DEFAULT_TIMEOUT = 3.0 +_DID_PREFIX = 'did' + + +class _DidResolverBase: + def __init__(self) -> None: + self._methods: t.Dict[str, t.Union[BaseResolver, AsyncBaseResolver]] = {} + + def _get_resolver_method(self, did: str) -> t.Union[BaseResolver, AsyncBaseResolver]: + parts = did.split(':') + if not parts: + raise PoorlyFormattedDidError(f'Invalid DID {did}') + + if parts[0] != _DID_PREFIX: + raise PoorlyFormattedDidError(f'Invalid DID {did}') + + if len(parts) < 2: + raise PoorlyFormattedDidError(f'Invalid DID {did}') + + method = parts[1] + if method not in self._methods: + raise UnsupportedDidMethodError(f'Invalid DID {did}') + + return self._methods[method] + + +class DidResolver(_DidResolverBase, BaseResolver): + """DID Resolver. + + Supported DID methods: PLC, Web. + + Args: + plc_url: PLC directory URL. + timeout: Request timeout. + cache: DID cache. + """ + + def __init__( + self, + plc_url: t.Optional[str] = None, + timeout: t.Optional[float] = None, + cache: t.Optional['DidBaseCache'] = None, + ) -> None: + super().__init__() + BaseResolver.__init__(self, cache) + + if plc_url is None: + plc_url = _BASE_PLC_URL + + if timeout is None: + timeout = _DEFAULT_TIMEOUT + + self._methods = {'plc': DidPlcResolver(plc_url, timeout, cache), 'web': DidWebResolver(timeout)} + + def resolve_without_validation(self, did: str) -> t.Optional[dict]: + """Resolve DID without validation. + + Args: + did: DID. + + Returns: + :obj:`dict`: DID document or ``None`` if DID not found. + """ + return self._get_resolver_method(did).resolve_without_validation(did) + + +class AsyncDidResolver(_DidResolverBase, AsyncBaseResolver): + """Asynchronous DID Resolver. + + Supported DID methods: PLC, Web. + + Args: + plc_url: PLC directory URL. + timeout: Request timeout. + cache: DID cache. + """ + + def __init__( + self, + plc_url: t.Optional[str] = None, + timeout: t.Optional[float] = None, + cache: t.Optional['AsyncDidBaseCache'] = None, + ) -> None: + super().__init__() + AsyncBaseResolver.__init__(self, cache) + + if plc_url is None: + plc_url = _BASE_PLC_URL + + if timeout is None: + timeout = _DEFAULT_TIMEOUT + + self._methods = {'plc': AsyncDidPlcResolver(plc_url, timeout, cache), 'web': AsyncDidWebResolver(timeout)} + + async def resolve_without_validation(self, did: str) -> t.Optional[dict]: + """Resolve DID without validation. + + Args: + did: DID. + + Returns: + :obj:`dict`: DID document or ``None`` if DID not found. + """ + return await self._get_resolver_method(did).resolve_without_validation(did) diff --git a/packages/atproto_identity/did/resolvers/__init__.py b/packages/atproto_identity/did/resolvers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/atproto_identity/did/resolvers/base_resolver.py b/packages/atproto_identity/did/resolvers/base_resolver.py new file mode 100644 index 00000000..3d51ddd4 --- /dev/null +++ b/packages/atproto_identity/did/resolvers/base_resolver.py @@ -0,0 +1,225 @@ +import typing as t +from abc import ABC, abstractmethod + +from atproto_core.did_doc import is_valid_did_doc, parse_did_doc + +from atproto_identity.did.atproto_data import ensure_atproto_document, ensure_atproto_key +from atproto_identity.exceptions import DidNotFoundError, PoorlyFormattedDidDocumentError + +if t.TYPE_CHECKING: + from atproto_core.did_doc import DidDocument + + from atproto_identity.cache.base_cache import AsyncDidBaseCache, DidBaseCache + from atproto_identity.did.models import AtprotoData + +_DID_KEY_PREFIX = 'did:key:' + + +class _BaseResolver: + @staticmethod + def validate_did_doc(did: str, value: dict) -> 'DidDocument': + # it performs double parsing, but it's ok for now + if not is_valid_did_doc(value): + raise PoorlyFormattedDidDocumentError(f'Invalid DID document for DID {did}') + + did_doc = parse_did_doc(value) + if did_doc.id != did: + raise PoorlyFormattedDidDocumentError(f'Invalid DID document for DID {did}') + + return did_doc + + +class BaseResolver(_BaseResolver, ABC): + def __init__(self, cache: 'DidBaseCache' = None) -> None: + self._cache = cache + + @abstractmethod + def resolve_without_validation(self, did: str) -> t.Optional[dict]: + raise NotImplementedError + + def resolve_no_cache(self, did: str) -> t.Optional['DidDocument']: + """Resolve DID without cache. + + Args: + did: DID. + + Returns: + :obj:`DidDocument`: DID document or ``None`` if not found. + """ + value = self.resolve_without_validation(did) + if value is None: + return None + + return self.validate_did_doc(did, value) + + def refresh_cache(self, did: str) -> None: + """Refresh cached DID. + + Args: + did: DID. + """ + if self._cache is None: + return + + self._cache.refresh(did, lambda: self.resolve_no_cache(did)) + + def resolve(self, did: str, force_refresh: bool = False) -> t.Optional['DidDocument']: + """Resolve DID. + + Args: + did: DID. + force_refresh: Force refresh cache. + + Returns: + :obj:`DidDocument`: DID document or ``None`` if not found. + """ + if self._cache and not force_refresh: + cached_result = self._cache.get(did) + if cached_result is not None and not cached_result.expired: + if cached_result.stale: + self.refresh_cache(did) + cached_result = self._cache.get(did) + + return cached_result.document + + did_doc = self.resolve_no_cache(did) + if did_doc is None: + if self._cache: + self._cache.delete(did) + + return None + + if self._cache: + self._cache.set(did, did_doc) + + return did_doc + + def ensure_resolve(self, did: str, force_refresh: bool = False) -> 'DidDocument': + """Ensure DID is resolved. + + Args: + did: DID. + force_refresh: Force refresh cache. + + Returns: + :obj:`DidDocument`: DID document. + + Raises: + :obj:`DidNotFoundError`: DID not found. + """ + did_doc = self.resolve(did, force_refresh) + if did_doc is None: + raise DidNotFoundError(f'Unable to resolve DID {did}') + + return did_doc + + def resolve_atproto_data(self, did: str, force_refresh: bool = False) -> 'AtprotoData': + """Not implemented yet.""" + did_doc = self.ensure_resolve(did, force_refresh) + return ensure_atproto_document(did_doc) + + def resolve_atproto_key(self, did: str, force_refresh: bool = False) -> str: + """Not implemented yet.""" + if did.startswith(_DID_KEY_PREFIX): + return did + + did_doc = self.ensure_resolve(did, force_refresh) + return ensure_atproto_key(did_doc) + + +class AsyncBaseResolver(_BaseResolver, ABC): + def __init__(self, cache: 'AsyncDidBaseCache' = None) -> None: + self._cache = cache + + @abstractmethod + async def resolve_without_validation(self, did: str) -> t.Optional[dict]: + raise NotImplementedError + + async def resolve_no_cache(self, did: str) -> t.Optional['DidDocument']: + """Resolve DID without cache. + + Args: + did: DID. + + Returns: + :obj:`DidDocument`: DID document or ``None`` if not found. + """ + value = await self.resolve_without_validation(did) + if value is None: + return None + + return self.validate_did_doc(did, value) + + async def refresh_cache(self, did: str) -> None: + """Refresh cached DID. + + Args: + did: DID. + """ + if self._cache is None: + return + + await self._cache.refresh(did, lambda: self.resolve_no_cache(did)) + + async def resolve(self, did: str, force_refresh: bool = False) -> t.Optional['DidDocument']: + """Resolve DID. + + Args: + did: DID. + force_refresh: Force refresh cache. + + Returns: + :obj:`DidDocument`: DID document or ``None`` if not found. + """ + if self._cache and not force_refresh: + cached_result = await self._cache.get(did) + if cached_result is not None and not cached_result.expired: + if cached_result.stale: + await self.refresh_cache(did) + cached_result = await self._cache.get(did) + + return cached_result.document + + did_doc = await self.resolve_no_cache(did) + if did_doc is None: + if self._cache: + await self._cache.delete(did) + + return None + + if self._cache: + await self._cache.set(did, did_doc) + + return did_doc + + async def ensure_resolve(self, did: str, force_refresh: bool = False) -> 'DidDocument': + """Ensure DID is resolved. + + Args: + did: DID. + force_refresh: Force refresh cache. + + Returns: + :obj:`DidDocument`: DID document. + + Raises: + :obj:`DidNotFoundError`: DID not found. + """ + did_doc = await self.resolve(did, force_refresh) + if did_doc is None: + raise DidNotFoundError(f'Unable to resolve DID {did}') + + return did_doc + + async def resolve_atproto_data(self, did: str, force_refresh: bool = False) -> 'AtprotoData': + """Not implemented yet.""" + did_doc = await self.ensure_resolve(did, force_refresh) + return ensure_atproto_document(did_doc) + + async def resolve_atproto_key(self, did: str, force_refresh: bool = False) -> str: + """Not implemented yet.""" + if did.startswith(_DID_KEY_PREFIX): + return did + + did_doc = await self.ensure_resolve(did, force_refresh) + return ensure_atproto_key(did_doc) diff --git a/packages/atproto_identity/did/resolvers/plc_resolver.py b/packages/atproto_identity/did/resolvers/plc_resolver.py new file mode 100644 index 00000000..98356a24 --- /dev/null +++ b/packages/atproto_identity/did/resolvers/plc_resolver.py @@ -0,0 +1,63 @@ +import typing as t + +import httpx + +from atproto_identity.did.resolvers.base_resolver import AsyncBaseResolver, BaseResolver +from atproto_identity.exceptions import DidPlcResolverError + +if t.TYPE_CHECKING: + from atproto_identity.cache.base_cache import AsyncDidBaseCache, DidBaseCache + + +class DidPlcResolver(BaseResolver): + def __init__( + self, + plc_url: t.Optional[str] = None, + timeout: t.Optional[float] = None, + cache: t.Optional['DidBaseCache'] = None, + ) -> None: + super().__init__(cache) + + self._plc_url = plc_url + self._timeout = timeout + + self._client = httpx.Client() + + def resolve_without_validation(self, did: str) -> t.Optional[dict]: + try: + response = self._client.get(f'{self._plc_url}/{did}', timeout=self._timeout) + + if response.status_code == 404: + return None + + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + raise DidPlcResolverError(f'Error resolving DID {did}') from e + + +class AsyncDidPlcResolver(AsyncBaseResolver): + def __init__( + self, + plc_url: t.Optional[str] = None, + timeout: t.Optional[float] = None, + cache: t.Optional['AsyncDidBaseCache'] = None, + ) -> None: + super().__init__(cache) + + self._plc_url = plc_url + self._timeout = timeout + + self._client = httpx.AsyncClient() + + async def resolve_without_validation(self, did: str) -> t.Optional[dict]: + try: + response = await self._client.get(f'{self._plc_url}/{did}', timeout=self._timeout) + + if response.status_code == 404: + return None + + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + raise DidPlcResolverError(f'Error resolving DID {did}') from e diff --git a/packages/atproto_identity/did/resolvers/web_resolver.py b/packages/atproto_identity/did/resolvers/web_resolver.py new file mode 100644 index 00000000..63cd6add --- /dev/null +++ b/packages/atproto_identity/did/resolvers/web_resolver.py @@ -0,0 +1,73 @@ +import typing as t + +import httpx + +from atproto_identity.did.resolvers.base_resolver import AsyncBaseResolver, BaseResolver +from atproto_identity.exceptions import DidWebResolverError, PoorlyFormattedDidError, UnsupportedDidWebPathError + +if t.TYPE_CHECKING: + from atproto_identity.cache.base_cache import AsyncDidBaseCache, DidBaseCache + +_DID_DOC_PATH = '/.well-known/did.json' + + +class _DidWebResolverBase: + @staticmethod + def _parse_web_did(did: str) -> str: + parsed_id = ':'.join(did.split(':')[2:]) + parts = parsed_id.split(':') + + if not parts: + raise PoorlyFormattedDidError(f'Invalid DID {did}') + + if len(parts) > 1: + raise UnsupportedDidWebPathError(f'Unsupported DID {did}') + + path = parts[0] + _DID_DOC_PATH + return f'https://{path}' + + +class DidWebResolver(_DidWebResolverBase, BaseResolver): + def __init__( + self, + timeout: t.Optional[float] = None, + cache: t.Optional['DidBaseCache'] = None, + ) -> None: + super().__init__(cache) + + self._timeout = timeout + + self._client = httpx.Client() + + def resolve_without_validation(self, did: str) -> dict: + url = self._parse_web_did(did) + + try: + response = self._client.get(url, timeout=self._timeout) + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + raise DidWebResolverError(f'Error resolving DID {did}') from e + + +class AsyncDidWebResolver(_DidWebResolverBase, AsyncBaseResolver): + def __init__( + self, + timeout: t.Optional[float] = None, + cache: t.Optional['AsyncDidBaseCache'] = None, + ) -> None: + super().__init__(cache) + + self._timeout = timeout + + self._client = httpx.AsyncClient() + + async def resolve_without_validation(self, did: str) -> dict: + url = self._parse_web_did(did) + + try: + response = await self._client.get(url, timeout=self._timeout) + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + raise DidWebResolverError(f'Error resolving DID {did}') from e diff --git a/packages/atproto_identity/exceptions.py b/packages/atproto_identity/exceptions.py new file mode 100644 index 00000000..069ebd43 --- /dev/null +++ b/packages/atproto_identity/exceptions.py @@ -0,0 +1,29 @@ +from atproto_core.exceptions import AtProtocolError + + +class DidPlcResolverError(AtProtocolError): + ... + + +class DidWebResolverError(AtProtocolError): + ... + + +class PoorlyFormattedDidError(AtProtocolError): + ... + + +class UnsupportedDidWebPathError(AtProtocolError): + ... + + +class UnsupportedDidMethodError(AtProtocolError): + ... + + +class PoorlyFormattedDidDocumentError(AtProtocolError): + ... + + +class DidNotFoundError(AtProtocolError): + ... diff --git a/packages/atproto_identity/handle/__init__.py b/packages/atproto_identity/handle/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/atproto_identity/handle/resolver.py b/packages/atproto_identity/handle/resolver.py new file mode 100644 index 00000000..aa629576 --- /dev/null +++ b/packages/atproto_identity/handle/resolver.py @@ -0,0 +1,233 @@ +import typing as t + +import dns.asyncresolver +import dns.resolver +import httpx +from dns.exception import DNSException + +from atproto_identity.exceptions import DidNotFoundError + +_ATPROTO_SUBDOMAIN = '_atproto' +_ATPROTO_RECORD_TYPE = 'TXT' +_ATPROTO_TXT_PREFIX = 'did=' +_ATPROTO_TXT_PREFIX_LEN = len(_ATPROTO_TXT_PREFIX) +_ATPROTO_WELL_KNOWN_PATH = '/.well-known/atproto-did' +_ATPROTO_DID_PREFIX = 'did:' + + +class _HandleResolverBase: + @staticmethod + def _find_text_record(answers: dns.resolver.Answer) -> t.Optional[str]: + for answer in answers: + for item in answer.strings: + value = item.decode('UTF-8') + if value.startswith(_ATPROTO_TXT_PREFIX): + return value[_ATPROTO_TXT_PREFIX_LEN:] + + return None + + @staticmethod + def _get_qname(handle: str) -> str: + return f'{_ATPROTO_SUBDOMAIN}.{handle}' + + @staticmethod + def _get_did_from_text_response(text: str) -> t.Optional[str]: + lines = text.splitlines() + if not lines: + return None + + first_line = lines[0].strip() + if first_line.startswith(_ATPROTO_DID_PREFIX): + return first_line + + return None + + +class HandleResolver(_HandleResolverBase): + """Handle Resolver. + + Args: + timeout: Request timeout. + backup_nameservers: Backup nameservers (for DNS resolve). + """ + + def __init__( + self, + timeout: t.Optional[float] = None, + backup_nameservers: t.Optional[t.List[str]] = None, + ) -> None: + self._timeout = timeout + self._backup_nameservers = backup_nameservers # TODO(MarshalX): implement + + self._dns_resolver = dns.resolver + self._http_client = httpx.Client() + + def resolve(self, handle: str) -> t.Optional[str]: + """Resolve handle to DID. + + Uses DNS and HTTP to resolve handle to DID. The first successful result will be returned. + + Resolve order: DNS -> HTTP. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID or ``None`` if handle not found. + """ + dns_resolve = self.resolve_dns(handle) + if dns_resolve: + return dns_resolve + + http_resolve = self.resolve_http(handle) + if http_resolve: + return http_resolve + + # TODO(MarshalX): add resolve DNS backup nameservers + return None + + def ensure_resolve(self, handle: str) -> str: + """Ensure handle is resolved to DID. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID. + + Raises: + :obj:`DidNotFoundError`: Handle not found. + """ + did = self.resolve(handle) + if not did: + raise DidNotFoundError(f'Unable to resolve handle: {handle}') + + return did + + def resolve_dns(self, handle: str) -> t.Optional[str]: + """Resolve handle to DID using DNS. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID or ``None`` if handle not found. + """ + try: + answers = self._dns_resolver.resolve(self._get_qname(handle), _ATPROTO_RECORD_TYPE, lifetime=self._timeout) + except DNSException: + return None + + return self._find_text_record(answers) + + def resolve_http(self, handle: str) -> t.Optional[str]: + """Resolve handle to DID using HTTP. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID or ``None`` if handle not found. + """ + try: + response = self._http_client.get(f'https://{handle}{_ATPROTO_WELL_KNOWN_PATH}', timeout=self._timeout) + response.raise_for_status() + return self._get_did_from_text_response(response.text) + except httpx.HTTPError: + return None + + +class AsyncHandleResolver(_HandleResolverBase): + """Asynchronous Handle Resolver. + + Args: + timeout: Request timeout. + backup_nameservers: Backup nameservers (for DNS resolve). + """ + + def __init__( + self, + timeout: t.Optional[float] = None, + backup_nameservers: t.Optional[t.List[str]] = None, + ) -> None: + self._timeout = timeout + self._backup_nameservers = backup_nameservers # TODO(MarshalX): implement + + self._dns_resolver = dns.asyncresolver + self._http_client = httpx.AsyncClient() + + async def resolve(self, handle: str) -> t.Optional[str]: + """Resolve handle to DID. + + Uses DNS and HTTP to resolve handle to DID. The first successful result will be returned. + + Resolve order: DNS -> HTTP. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID or ``None`` if handle not found. + """ + dns_resolve = await self.resolve_dns(handle) + if dns_resolve: + return dns_resolve + + http_resolve = await self.resolve_http(handle) + if http_resolve: + return http_resolve + + # TODO(MarshalX): add resolve DNS backup nameservers + return None + + async def ensure_resolve(self, handle: str) -> str: + """Ensure handle is resolved to DID. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID. + + Raises: + :obj:`DidNotFoundError`: Handle not found. + """ + did = await self.resolve(handle) + if not did: + raise DidNotFoundError(f'Unable to resolve handle: {handle}') + + return did + + async def resolve_dns(self, handle: str) -> t.Optional[str]: + """Resolve handle to DID using DNS. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID or ``None`` if handle not found. + """ + try: + answers = await self._dns_resolver.resolve( + self._get_qname(handle), _ATPROTO_RECORD_TYPE, lifetime=self._timeout + ) + except DNSException: + return None + + return self._find_text_record(answers) + + async def resolve_http(self, handle: str) -> t.Optional[str]: + """Resolve handle to DID using HTTP. + + Args: + handle: Handle. + + Returns: + :obj:`str`: DID or ``None`` if handle not found. + """ + try: + response = await self._http_client.get(f'https://{handle}{_ATPROTO_WELL_KNOWN_PATH}', timeout=self._timeout) + response.raise_for_status() + return self._get_did_from_text_response(response.text) + except httpx.HTTPError: + return None diff --git a/packages/atproto_identity/resolver.py b/packages/atproto_identity/resolver.py new file mode 100644 index 00000000..b51e546a --- /dev/null +++ b/packages/atproto_identity/resolver.py @@ -0,0 +1,95 @@ +import typing as t + +from atproto_identity.did.resolver import AsyncDidResolver, DidResolver +from atproto_identity.handle.resolver import AsyncHandleResolver, HandleResolver + +if t.TYPE_CHECKING: + from atproto_identity.cache.base_cache import AsyncDidBaseCache, DidBaseCache + + +class IdResolver: + """Identity Resolver. + + This resolver is used to resolve identities. + DID and Handle identifies are supported. + + Note: + Default PLC directory URL is https://plc.directory. + Default request timeout is 3 seconds. + + Args: + plc_url: PLC directory URL. + timeout: Request timeout. + cache: DID cache. + """ + + def __init__( + self, + plc_url: t.Optional[str] = None, + timeout: t.Optional[float] = None, + cache: t.Optional['DidBaseCache'] = None, + backup_nameservers: t.Optional[t.List[str]] = None, + ) -> None: + self._handle = HandleResolver(timeout, backup_nameservers) + self._did = DidResolver(plc_url, timeout, cache) + + @property + def handle(self) -> HandleResolver: + """Handle Resolver. + + This resolver is used to resolve handles. + """ + return self._handle + + @property + def did(self) -> DidResolver: + """DID Resolver. + + This resolver is used to resolve DIDs. + PLC and Web DID methods are supported. + """ + return self._did + + +class AsyncIdResolver: + """Asynchronous Identity Resolver. + + This resolver is used to resolve identities. + DID and Handle identifies are supported. + + Note: + Default PLC directory URL is https://plc.directory. + Default request timeout is 3 seconds. + + Args: + plc_url: PLC directory URL. + timeout: Request timeout. + cache: DID cache. + """ + + def __init__( + self, + plc_url: t.Optional[str] = None, + timeout: t.Optional[float] = None, + cache: t.Optional['AsyncDidBaseCache'] = None, + backup_nameservers: t.Optional[t.List[str]] = None, + ) -> None: + self._handle = AsyncHandleResolver(timeout, backup_nameservers) + self._did = AsyncDidResolver(plc_url, timeout, cache) + + @property + def handle(self) -> AsyncHandleResolver: + """Handle Resolver. + + This resolver is used to resolve handles. + """ + return self._handle + + @property + def did(self) -> AsyncDidResolver: + """DID Resolver. + + This resolver is used to resolve DIDs. + PLC and Web DID methods are supported. + """ + return self._did diff --git a/poetry.lock b/poetry.lock index 930bfd14..f90edd46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -241,6 +241,98 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dnspython" +version = "2.3.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, + {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, +] + +[package.extras] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<40.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] +doq = ["aioquic (>=0.9.20)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.23)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + [[package]] name = "docutils" version = "0.19" @@ -752,19 +844,19 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.5.2" +version = "2.5.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, ] [package.dependencies] annotated-types = ">=0.4.0" importlib-metadata = {version = "*", markers = "python_version == \"3.7\""} -pydantic-core = "2.14.5" +pydantic-core = "2.14.6" typing-extensions = ">=4.6.1" [package.extras] @@ -772,116 +864,116 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.5" +version = "2.14.6" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, - {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, - {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, ] [package.dependencies] @@ -960,6 +1052,25 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "python-dotenv" version = "0.21.1" @@ -1492,4 +1603,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7.1,<3.13" -content-hash = "cf2e5c9c6eb109d4131bbcbc6c327cfd4f46644654331c8d679220deaafedf0d" +content-hash = "3f3d04d2fdff122758173acf448b5d3a721efe6c23f06b3bf529482969767e33" diff --git a/pyproject.toml b/pyproject.toml index a3d01b6c..6e76463c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ packages = [ { include = "atproto_codegen", from = "packages" }, { include = "atproto_core", from = "packages" }, { include = "atproto_firehose", from = "packages" }, + { include = "atproto_identity", from = "packages" }, { include = "atproto_lexicon", from = "packages" }, { include = "atproto_server", from = "packages" }, ] @@ -55,6 +56,7 @@ click = ">=8.1.3,<9" websockets = ">=11.0.3,<13" pydantic = ">=2.0,<3.0" libipld = ">=1.0.0,<2.0.0" +dnspython = ">=2.3.0,<3" [tool.poetry.group.dev.dependencies] ruff = "0.1.2" @@ -71,6 +73,8 @@ sphinxext-opengraph = "0.7.5" [tool.poetry.group.test.dependencies] pytest = ">=7.3.1,<7.4.0" +pytest-asyncio = ">=0.21.0" +coverage = ">=7.2.7" [tool.poetry-dynamic-versioning] # poetry self add "poetry-dynamic-versioning[plugin]" diff --git a/tests/test_atproto_client/__init__.py b/tests/test_atproto_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_atproto_client/models/__init__.py b/tests/test_atproto_client/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/fetch_test_data.py b/tests/test_atproto_client/models/fetch_test_data.py similarity index 100% rename from tests/models/fetch_test_data.py rename to tests/test_atproto_client/models/fetch_test_data.py diff --git a/tests/models/test_data/custom_record.json b/tests/test_atproto_client/models/test_data/custom_record.json similarity index 100% rename from tests/models/test_data/custom_record.json rename to tests/test_atproto_client/models/test_data/custom_record.json diff --git a/tests/models/test_data/did_doc.json b/tests/test_atproto_client/models/test_data/did_doc.json similarity index 100% rename from tests/models/test_data/did_doc.json rename to tests/test_atproto_client/models/test_data/did_doc.json diff --git a/tests/models/test_data/extended_like_record.json b/tests/test_atproto_client/models/test_data/extended_like_record.json similarity index 100% rename from tests/models/test_data/extended_like_record.json rename to tests/test_atproto_client/models/test_data/extended_like_record.json diff --git a/tests/models/test_data/extended_post_record.json b/tests/test_atproto_client/models/test_data/extended_post_record.json similarity index 100% rename from tests/models/test_data/extended_post_record.json rename to tests/test_atproto_client/models/test_data/extended_post_record.json diff --git a/tests/models/test_data/feed_record.json b/tests/test_atproto_client/models/test_data/feed_record.json similarity index 100% rename from tests/models/test_data/feed_record.json rename to tests/test_atproto_client/models/test_data/feed_record.json diff --git a/tests/models/test_data/get_follows.json b/tests/test_atproto_client/models/test_data/get_follows.json similarity index 100% rename from tests/models/test_data/get_follows.json rename to tests/test_atproto_client/models/test_data/get_follows.json diff --git a/tests/models/test_data/like_record.json b/tests/test_atproto_client/models/test_data/like_record.json similarity index 100% rename from tests/models/test_data/like_record.json rename to tests/test_atproto_client/models/test_data/like_record.json diff --git a/tests/models/test_data/post_record.json b/tests/test_atproto_client/models/test_data/post_record.json similarity index 100% rename from tests/models/test_data/post_record.json rename to tests/test_atproto_client/models/test_data/post_record.json diff --git a/tests/models/test_data/resolve_handle.json b/tests/test_atproto_client/models/test_data/resolve_handle.json similarity index 100% rename from tests/models/test_data/resolve_handle.json rename to tests/test_atproto_client/models/test_data/resolve_handle.json diff --git a/tests/models/test_data/thread_view_post_with_embed_media.json b/tests/test_atproto_client/models/test_data/thread_view_post_with_embed_media.json similarity index 100% rename from tests/models/test_data/thread_view_post_with_embed_media.json rename to tests/test_atproto_client/models/test_data/thread_view_post_with_embed_media.json diff --git a/tests/test_atproto_client/models/tests/__init__.py b/tests/test_atproto_client/models/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/tests/test_blob_ref.py b/tests/test_atproto_client/models/tests/test_blob_ref.py similarity index 100% rename from tests/models/tests/test_blob_ref.py rename to tests/test_atproto_client/models/tests/test_blob_ref.py diff --git a/tests/models/tests/test_changed_lexicon_compatability.py b/tests/test_atproto_client/models/tests/test_changed_lexicon_compatability.py similarity index 100% rename from tests/models/tests/test_changed_lexicon_compatability.py rename to tests/test_atproto_client/models/tests/test_changed_lexicon_compatability.py diff --git a/tests/models/tests/test_custom_record.py b/tests/test_atproto_client/models/tests/test_custom_record.py similarity index 96% rename from tests/models/tests/test_custom_record.py rename to tests/test_atproto_client/models/tests/test_custom_record.py index 06dcc2f5..ef443b5a 100644 --- a/tests/models/tests/test_custom_record.py +++ b/tests/test_atproto_client/models/tests/test_custom_record.py @@ -2,7 +2,7 @@ from atproto_client.models import get_model_as_dict, get_or_create from atproto_client.models.dot_dict import DotDict -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_did_doc.py b/tests/test_atproto_client/models/tests/test_did_doc.py similarity index 93% rename from tests/models/tests/test_did_doc.py rename to tests/test_atproto_client/models/tests/test_did_doc.py index dc558676..e99b0fc2 100644 --- a/tests/models/tests/test_did_doc.py +++ b/tests/test_atproto_client/models/tests/test_did_doc.py @@ -2,7 +2,7 @@ from atproto_client.models import get_or_create from atproto_client.models.dot_dict import DotDict -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_dot_dict.py b/tests/test_atproto_client/models/tests/test_dot_dict.py similarity index 100% rename from tests/models/tests/test_dot_dict.py rename to tests/test_atproto_client/models/tests/test_dot_dict.py diff --git a/tests/models/tests/test_extended_like_record.py b/tests/test_atproto_client/models/tests/test_extended_like_record.py similarity index 95% rename from tests/models/tests/test_extended_like_record.py rename to tests/test_atproto_client/models/tests/test_extended_like_record.py index 96919914..dc60cfb2 100644 --- a/tests/models/tests/test_extended_like_record.py +++ b/tests/test_atproto_client/models/tests/test_extended_like_record.py @@ -1,7 +1,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_extended_post_record.py b/tests/test_atproto_client/models/tests/test_extended_post_record.py similarity index 95% rename from tests/models/tests/test_extended_post_record.py rename to tests/test_atproto_client/models/tests/test_extended_post_record.py index 65fb9f6e..d89d66d7 100644 --- a/tests/models/tests/test_extended_post_record.py +++ b/tests/test_atproto_client/models/tests/test_extended_post_record.py @@ -1,7 +1,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_feed_record.py b/tests/test_atproto_client/models/tests/test_feed_record.py similarity index 97% rename from tests/models/tests/test_feed_record.py rename to tests/test_atproto_client/models/tests/test_feed_record.py index 28c05e08..3bc6c510 100644 --- a/tests/models/tests/test_feed_record.py +++ b/tests/test_atproto_client/models/tests/test_feed_record.py @@ -4,7 +4,7 @@ from atproto_client.models.blob_ref import BlobRef from pydantic import ValidationError -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_get_follows.py b/tests/test_atproto_client/models/tests/test_get_follows.py similarity index 96% rename from tests/models/tests/test_get_follows.py rename to tests/test_atproto_client/models/tests/test_get_follows.py index 0c990e33..e056a18b 100644 --- a/tests/models/tests/test_get_follows.py +++ b/tests/test_atproto_client/models/tests/test_get_follows.py @@ -3,7 +3,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_is_record_type.py b/tests/test_atproto_client/models/tests/test_is_record_type.py similarity index 94% rename from tests/models/tests/test_is_record_type.py rename to tests/test_atproto_client/models/tests/test_is_record_type.py index 451df0d4..2164c99c 100644 --- a/tests/models/tests/test_is_record_type.py +++ b/tests/test_atproto_client/models/tests/test_is_record_type.py @@ -2,7 +2,7 @@ from atproto_client.models import get_or_create, is_record_type from atproto_client.models.dot_dict import DotDict -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_correct_data() -> dict: diff --git a/tests/models/tests/test_like_record.py b/tests/test_atproto_client/models/tests/test_like_record.py similarity index 97% rename from tests/models/tests/test_like_record.py rename to tests/test_atproto_client/models/tests/test_like_record.py index 4f3f153b..b4924290 100644 --- a/tests/models/tests/test_like_record.py +++ b/tests/test_atproto_client/models/tests/test_like_record.py @@ -1,7 +1,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_post_record.py b/tests/test_atproto_client/models/tests/test_post_record.py similarity index 97% rename from tests/models/tests/test_post_record.py rename to tests/test_atproto_client/models/tests/test_post_record.py index b61d73fa..695f450f 100644 --- a/tests/models/tests/test_post_record.py +++ b/tests/test_atproto_client/models/tests/test_post_record.py @@ -1,7 +1,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_resolve_handle.py b/tests/test_atproto_client/models/tests/test_resolve_handle.py similarity index 93% rename from tests/models/tests/test_resolve_handle.py rename to tests/test_atproto_client/models/tests/test_resolve_handle.py index 17635f77..449befe0 100644 --- a/tests/models/tests/test_resolve_handle.py +++ b/tests/test_atproto_client/models/tests/test_resolve_handle.py @@ -1,7 +1,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/test_thread_view_post_with_embed_media.py b/tests/test_atproto_client/models/tests/test_thread_view_post_with_embed_media.py similarity index 95% rename from tests/models/tests/test_thread_view_post_with_embed_media.py rename to tests/test_atproto_client/models/tests/test_thread_view_post_with_embed_media.py index 5969cdd9..5b0022ed 100644 --- a/tests/models/tests/test_thread_view_post_with_embed_media.py +++ b/tests/test_atproto_client/models/tests/test_thread_view_post_with_embed_media.py @@ -1,7 +1,7 @@ from atproto_client import models from atproto_client.models import get_model_as_dict, get_or_create -from tests.models.tests.utils import load_data_from_file +from tests.test_atproto_client.models.tests.utils import load_data_from_file def load_test_data() -> dict: diff --git a/tests/models/tests/utils.py b/tests/test_atproto_client/models/tests/utils.py similarity index 100% rename from tests/models/tests/utils.py rename to tests/test_atproto_client/models/tests/utils.py diff --git a/tests/test_atproto_core/__init__.py b/tests/test_atproto_core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_nsid.py b/tests/test_atproto_core/test_nsid.py similarity index 100% rename from tests/test_nsid.py rename to tests/test_atproto_core/test_nsid.py diff --git a/tests/test_uri.py b/tests/test_atproto_core/test_uri.py similarity index 100% rename from tests/test_uri.py rename to tests/test_atproto_core/test_uri.py diff --git a/tests/test_atproto_identity/__init__.py b/tests/test_atproto_identity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_atproto_identity/test_async_did_resolver.py b/tests/test_atproto_identity/test_async_did_resolver.py new file mode 100644 index 00000000..f3190656 --- /dev/null +++ b/tests/test_atproto_identity/test_async_did_resolver.py @@ -0,0 +1,49 @@ +import pytest +from atproto_identity.did.resolver import AsyncDidResolver +from atproto_identity.exceptions import DidNotFoundError, DidWebResolverError, UnsupportedDidWebPathError + +# THESE TESTS ARE NOT MOCKED WITH TEST SERVERS. IT PERFORMS REAL REQUESTS TO THE INTERNET. + + +@pytest.mark.asyncio +async def test_did_resolver_with_web_feed() -> None: + feed_url = 'feed.atproto.blue' + gen_endpoint = f'https://{feed_url}' + did_web = f'did:web:{feed_url}' + + did_doc = await AsyncDidResolver().ensure_resolve(did_web) + + assert did_doc.id == did_web + assert did_doc.get_feed_gen_endpoint() == gen_endpoint + + +@pytest.mark.asyncio +async def test_did_resolver_with_invalid_did_web() -> None: + with pytest.raises(UnsupportedDidWebPathError): + await AsyncDidResolver().ensure_resolve('did:web:feed.atproto.blue:lol:kek') + + +@pytest.mark.asyncio +async def test_did_resolver_with_unknown_did_web() -> None: + with pytest.raises(DidWebResolverError): + await AsyncDidResolver().ensure_resolve('did:web:feed123.atproto.blue') + + +@pytest.mark.asyncio +async def test_did_resolver_with_did_plc() -> None: + expected_handle = 'test.marshal.dev' + expected_key_type = 'Multikey' + expected_did_plc = 'did:plc:kvwvcn5iqfooopmyzvb4qzba' + + did_doc = await AsyncDidResolver().ensure_resolve(expected_did_plc) + + assert did_doc.id == expected_did_plc + assert did_doc.get_handle() == expected_handle + assert did_doc.get_signing_key().type == expected_key_type + + +@pytest.mark.asyncio +async def test_did_resolver_with_unknown_did_plc() -> None: + with pytest.raises(DidNotFoundError): + did_plc = 'did:plc:kvwvcn5iqfooopmyzvb41234' + await AsyncDidResolver().ensure_resolve(did_plc) diff --git a/tests/test_atproto_identity/test_async_did_resolver_cache.py b/tests/test_atproto_identity/test_async_did_resolver_cache.py new file mode 100644 index 00000000..d82ff8d6 --- /dev/null +++ b/tests/test_atproto_identity/test_async_did_resolver_cache.py @@ -0,0 +1,86 @@ +import pytest +from atproto_identity.cache.in_memory_cache import AsyncDidInMemoryCache +from atproto_identity.did.resolver import AsyncDidResolver + +# THESE TESTS ARE NOT MOCKED WITH TEST SERVERS. IT PERFORMS REAL REQUESTS TO THE INTERNET. + + +@pytest.mark.asyncio +async def test_did_resolver_cache_with_web_feed() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = AsyncDidInMemoryCache() + resolver = AsyncDidResolver(cache=cache) + did_doc = await resolver.ensure_resolve(did_web) + + expected_object_id = id(did_doc) + + for _ in range(10): + new_did_doc = await resolver.ensure_resolve(did_web) + # Check that the object is cached (same object id) + assert expected_object_id == id(new_did_doc) + + cached_did_doc = (await cache.get(did_web)).document + assert cached_did_doc is did_doc + + +@pytest.mark.asyncio +async def test_did_resolver_cache_with_did_plc() -> None: + did_plc = 'did:plc:kvwvcn5iqfooopmyzvb4qzba' + + cache = AsyncDidInMemoryCache() + resolver = AsyncDidResolver(cache=cache) + did_doc = await resolver.ensure_resolve(did_plc) + + expected_object_id = id(did_doc) + for _ in range(10): + new_did_doc = await resolver.ensure_resolve(did_plc) + # Check that the object is cached (same object id) + assert expected_object_id == id(new_did_doc) + + cached_did_doc = (await cache.get(did_plc)).document + assert cached_did_doc is did_doc + + +@pytest.mark.asyncio +async def test_did_resolver_resolve_no_cache() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = AsyncDidInMemoryCache() + resolver = AsyncDidResolver(cache=cache) + + cached_did_doc = await resolver.ensure_resolve(did_web) + new_did_doc = await resolver.resolve_no_cache(did_web) + + assert id(cached_did_doc) != id(new_did_doc) + + +@pytest.mark.asyncio +async def test_did_resolver_resolve_force() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = AsyncDidInMemoryCache() + resolver = AsyncDidResolver(cache=cache) + + cached_did_doc = await resolver.ensure_resolve(did_web) + new_did_doc = await resolver.ensure_resolve(did_web, force_refresh=True) + + assert id(cached_did_doc) != id(new_did_doc) + + +@pytest.mark.asyncio +async def test_did_resolver_refresh_cache() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = AsyncDidInMemoryCache() + resolver = AsyncDidResolver(cache=cache) + + cached_did_doc = await resolver.ensure_resolve(did_web) + await resolver.refresh_cache(did_web) + new_did_doc = await resolver.ensure_resolve(did_web) + + assert id(cached_did_doc) != id(new_did_doc) diff --git a/tests/test_atproto_identity/test_async_handle_resolver.py b/tests/test_atproto_identity/test_async_handle_resolver.py new file mode 100644 index 00000000..c9fb080e --- /dev/null +++ b/tests/test_atproto_identity/test_async_handle_resolver.py @@ -0,0 +1,30 @@ +import pytest +from atproto_identity.exceptions import DidNotFoundError +from atproto_identity.handle.resolver import AsyncHandleResolver + +# THESE TESTS ARE NOT MOCKED WITH TEST SERVERS. IT PERFORMS REAL REQUESTS TO THE INTERNET. + + +@pytest.mark.asyncio +async def test_handle_resolver() -> None: + expected_handle = 'test.marshal.dev' + expected_did_plc = 'did:plc:kvwvcn5iqfooopmyzvb4qzba' + + did_plc = await AsyncHandleResolver().ensure_resolve(expected_handle) + assert did_plc == expected_did_plc + + +@pytest.mark.asyncio +async def test_handle_resolver_with_invalid_handle_url() -> None: + expected_handle = 'test123.marshal.dev' + + with pytest.raises(DidNotFoundError): + await AsyncHandleResolver().ensure_resolve(expected_handle) + + +@pytest.mark.asyncio +async def test_handle_resolver_with_not_existing_well_know() -> None: + expected_handle = 'feed.atproto.blue' + + with pytest.raises(DidNotFoundError): + await AsyncHandleResolver().ensure_resolve(expected_handle) diff --git a/tests/test_atproto_identity/test_async_id_resolver.py b/tests/test_atproto_identity/test_async_id_resolver.py new file mode 100644 index 00000000..8375f74b --- /dev/null +++ b/tests/test_atproto_identity/test_async_id_resolver.py @@ -0,0 +1,13 @@ +from atproto_identity.did.resolver import AsyncDidResolver +from atproto_identity.handle.resolver import AsyncHandleResolver +from atproto_identity.resolver import AsyncIdResolver + + +def test_id_resolver_did_resolver() -> None: + did_resolver = AsyncIdResolver().did + assert isinstance(did_resolver, AsyncDidResolver) + + +def test_id_resolver_handle_resolver() -> None: + handle_resolver = AsyncIdResolver().handle + assert isinstance(handle_resolver, AsyncHandleResolver) diff --git a/tests/test_atproto_identity/test_did_resolver.py b/tests/test_atproto_identity/test_did_resolver.py new file mode 100644 index 00000000..f4471224 --- /dev/null +++ b/tests/test_atproto_identity/test_did_resolver.py @@ -0,0 +1,44 @@ +import pytest +from atproto_identity.did.resolver import DidResolver +from atproto_identity.exceptions import DidNotFoundError, DidWebResolverError, UnsupportedDidWebPathError + +# THESE TESTS ARE NOT MOCKED WITH TEST SERVERS. IT PERFORMS REAL REQUESTS TO THE INTERNET. + + +def test_did_resolver_with_web_feed() -> None: + feed_url = 'feed.atproto.blue' + gen_endpoint = f'https://{feed_url}' + did_web = f'did:web:{feed_url}' + + did_doc = DidResolver().ensure_resolve(did_web) + + assert did_doc.id == did_web + assert did_doc.get_feed_gen_endpoint() == gen_endpoint + + +def test_did_resolver_with_invalid_did_web() -> None: + with pytest.raises(UnsupportedDidWebPathError): + DidResolver().ensure_resolve('did:web:feed.atproto.blue:lol:kek') + + +def test_did_resolver_with_unknown_did_web() -> None: + with pytest.raises(DidWebResolverError): + DidResolver().ensure_resolve('did:web:feed123.atproto.blue') + + +def test_did_resolver_with_did_plc() -> None: + expected_handle = 'test.marshal.dev' + expected_key_type = 'Multikey' + expected_did_plc = 'did:plc:kvwvcn5iqfooopmyzvb4qzba' + + did_doc = DidResolver().ensure_resolve(expected_did_plc) + + assert did_doc.id == expected_did_plc + assert did_doc.get_handle() == expected_handle + assert did_doc.get_signing_key().type == expected_key_type + + +def test_did_resolver_with_unknown_did_plc() -> None: + with pytest.raises(DidNotFoundError): + did_plc = 'did:plc:kvwvcn5iqfooopmyzvb41234' + DidResolver().ensure_resolve(did_plc) diff --git a/tests/test_atproto_identity/test_did_resolver_cache.py b/tests/test_atproto_identity/test_did_resolver_cache.py new file mode 100644 index 00000000..ed565ec6 --- /dev/null +++ b/tests/test_atproto_identity/test_did_resolver_cache.py @@ -0,0 +1,80 @@ +from atproto_identity.cache.in_memory_cache import DidInMemoryCache +from atproto_identity.did.resolver import DidResolver + +# THESE TESTS ARE NOT MOCKED WITH TEST SERVERS. IT PERFORMS REAL REQUESTS TO THE INTERNET. + + +def test_did_resolver_cache_with_web_feed() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = DidInMemoryCache() + resolver = DidResolver(cache=cache) + did_doc = resolver.ensure_resolve(did_web) + + expected_object_id = id(did_doc) + + for _ in range(10): + new_did_doc = resolver.ensure_resolve(did_web) + # Check that the object is cached (same object id) + assert expected_object_id == id(new_did_doc) + + cached_did_doc = (cache.get(did_web)).document + assert cached_did_doc is did_doc + + +def test_did_resolver_cache_with_did_plc() -> None: + did_plc = 'did:plc:kvwvcn5iqfooopmyzvb4qzba' + + cache = DidInMemoryCache() + resolver = DidResolver(cache=cache) + did_doc = resolver.ensure_resolve(did_plc) + + expected_object_id = id(did_doc) + for _ in range(10): + new_did_doc = resolver.ensure_resolve(did_plc) + # Check that the object is cached (same object id) + assert expected_object_id == id(new_did_doc) + + cached_did_doc = cache.get(did_plc).document + assert cached_did_doc is did_doc + + +def test_did_resolver_resolve_no_cache() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = DidInMemoryCache() + resolver = DidResolver(cache=cache) + + cached_did_doc = resolver.ensure_resolve(did_web) + new_did_doc = resolver.resolve_no_cache(did_web) + + assert id(cached_did_doc) != id(new_did_doc) + + +def test_did_resolver_resolve_force() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = DidInMemoryCache() + resolver = DidResolver(cache=cache) + + cached_did_doc = resolver.ensure_resolve(did_web) + new_did_doc = resolver.ensure_resolve(did_web, force_refresh=True) + + assert id(cached_did_doc) != id(new_did_doc) + + +def test_did_resolver_refresh_cache() -> None: + feed_url = 'feed.atproto.blue' + did_web = f'did:web:{feed_url}' + + cache = DidInMemoryCache() + resolver = DidResolver(cache=cache) + + cached_did_doc = resolver.ensure_resolve(did_web) + resolver.refresh_cache(did_web) + new_did_doc = resolver.ensure_resolve(did_web) + + assert id(cached_did_doc) != id(new_did_doc) diff --git a/tests/test_atproto_identity/test_handle_resolver.py b/tests/test_atproto_identity/test_handle_resolver.py new file mode 100644 index 00000000..07303b7b --- /dev/null +++ b/tests/test_atproto_identity/test_handle_resolver.py @@ -0,0 +1,27 @@ +import pytest +from atproto_identity.exceptions import DidNotFoundError +from atproto_identity.handle.resolver import HandleResolver + +# THESE TESTS ARE NOT MOCKED WITH TEST SERVERS. IT PERFORMS REAL REQUESTS TO THE INTERNET. + + +def test_handle_resolver() -> None: + expected_handle = 'test.marshal.dev' + expected_did_plc = 'did:plc:kvwvcn5iqfooopmyzvb4qzba' + + did_plc = HandleResolver().ensure_resolve(expected_handle) + assert did_plc == expected_did_plc + + +def test_handle_resolver_with_invalid_handle_url() -> None: + expected_handle = 'test123.marshal.dev' + + with pytest.raises(DidNotFoundError): + HandleResolver().ensure_resolve(expected_handle) + + +def test_handle_resolver_with_not_existing_well_know() -> None: + expected_handle = 'feed.atproto.blue' + + with pytest.raises(DidNotFoundError): + HandleResolver().ensure_resolve(expected_handle) diff --git a/tests/test_atproto_identity/test_id_resolver.py b/tests/test_atproto_identity/test_id_resolver.py new file mode 100644 index 00000000..dca1f2a7 --- /dev/null +++ b/tests/test_atproto_identity/test_id_resolver.py @@ -0,0 +1,13 @@ +from atproto_identity.did.resolver import DidResolver +from atproto_identity.handle.resolver import HandleResolver +from atproto_identity.resolver import IdResolver + + +def test_id_resolver_did_resolver() -> None: + did_resolver = IdResolver().did + assert isinstance(did_resolver, DidResolver) + + +def test_id_resolver_handle_resolver() -> None: + handle_resolver = IdResolver().handle + assert isinstance(handle_resolver, HandleResolver) diff --git a/tests/test_atproto_lexicon/__init__.py b/tests/test_atproto_lexicon/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_lexicon_parser.py b/tests/test_atproto_lexicon/test_lexicon_parser.py similarity index 100% rename from tests/test_lexicon_parser.py rename to tests/test_atproto_lexicon/test_lexicon_parser.py