diff --git a/README.md b/README.md index 2b28d1e8..119b6451 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ informal introduction to the features and their implementation. - [Usage](#usage) - [Client](#client) - [Data Conversion](#data-conversion) + - [Pydantic Support](#pydantic-support) - [Custom Type Data Conversion](#custom-type-data-conversion) - [Workers](#workers) - [Workflows](#workflows) @@ -84,7 +85,6 @@ informal introduction to the features and their implementation. - [Extending Restricted Classes](#extending-restricted-classes) - [Certain Standard Library Calls on Restricted Objects](#certain-standard-library-calls-on-restricted-objects) - [is_subclass of ABC-based Restricted Classes](#is_subclass-of-abc-based-restricted-classes) - - [Compiled Pydantic Sometimes Using Wrong Types](#compiled-pydantic-sometimes-using-wrong-types) - [Activities](#activities) - [Definition](#definition-1) - [Types of Activities](#types-of-activities) @@ -298,10 +298,10 @@ other_ns_client = Client(**config) #### Data Conversion Data converters are used to convert raw Temporal payloads to/from actual Python types. A custom data converter of type -`temporalio.converter.DataConverter` can be set via the `data_converter` client parameter. Data converters are a -combination of payload converters, payload codecs, and failure converters. Payload converters convert Python values -to/from serialized bytes. Payload codecs convert bytes to bytes (e.g. for compression or encryption). Failure converters -convert exceptions to/from serialized failures. +`temporalio.converter.DataConverter` can be set via the `data_converter` parameter of the `Client` constructor. Data +converters are a combination of payload converters, payload codecs, and failure converters. Payload converters convert +Python values to/from serialized bytes. Payload codecs convert bytes to bytes (e.g. for compression or encryption). +Failure converters convert exceptions to/from serialized failures. The default data converter supports converting multiple types including: @@ -312,22 +312,39 @@ The default data converter supports converting multiple types including: * Anything that [`json.dump`](https://docs.python.org/3/library/json.html#json.dump) supports natively * [dataclasses](https://docs.python.org/3/library/dataclasses.html) * Iterables including ones JSON dump may not support by default, e.g. `set` - * Any class with a `dict()` method and a static `parse_obj()` method, e.g. - [Pydantic models](https://pydantic-docs.helpmanual.io/usage/models) - * The default data converter is deprecated for Pydantic models and will warn if used since not all fields work. - See [this sample](https://github.com/temporalio/samples-python/tree/main/pydantic_converter) for the recommended - approach. * [IntEnum, StrEnum](https://docs.python.org/3/library/enum.html) based enumerates * [UUID](https://docs.python.org/3/library/uuid.html) -This notably doesn't include any `date`, `time`, or `datetime` objects as they may not work across SDKs. +To use pydantic model instances, see [](#pydantic-support). -Users are strongly encouraged to use a single `dataclass` for parameter and return types so fields with defaults can be -easily added without breaking compatibility. +`datetime.date`, `datetime.time`, and `datetime.datetime` can only be used with the Pydantic data converter. + +Although workflows, updates, signals, and queries can all be defined with multiple input parameters, users are strongly +encouraged to use a single `dataclass` or Pydantic model parameter, so that fields with defaults can be easily added +without breaking compatibility. Similar advice applies to return values. Classes with generics may not have the generics properly resolved. The current implementation does not have generic type resolution. Users should use concrete types. +##### Pydantic Support + +To use Pydantic model instances, install Pydantic and set the Pydantic data converter when creating client instances: + +```python +from temporalio.contrib.pydantic import pydantic_data_converter + +client = Client(data_converter=pydantic_data_converter, ...) +``` + +This data converter supports conversion of all types supported by Pydantic to and from JSON. + +In addition to Pydantic models, these include all `json.dump`-able types, various non-`json.dump`-able standard library +types such as dataclasses, types from the datetime module, sets, UUID, etc, and custom types composed of any of these. + +Pydantic v1 is not supported by this data converter. If you are not yet able to upgrade from Pydantic v1, see +https://github.com/temporalio/samples-python/tree/main/pydantic_converter/v1 for limited v1 support. + + ##### Custom Type Data Conversion For converting from JSON, the workflow/activity type hint is taken into account to convert to the proper type. Care has @@ -1133,15 +1150,6 @@ Due to [https://bugs.python.org/issue44847](https://bugs.python.org/issue44847), checked to see if they are subclasses of another via `is_subclass` may fail (see also [this wrapt issue](https://github.com/GrahamDumpleton/wrapt/issues/130)). -###### Compiled Pydantic Sometimes Using Wrong Types - -If the Pydantic dependency is in compiled form (the default) and you are using a Pydantic model inside a workflow -sandbox that uses a `datetime` type, it will grab the wrong validator and use `date` instead. This is because our -patched form of `issubclass` is bypassed by compiled Pydantic. - -To work around, either don't use `datetime`-based Pydantic model fields in workflows, or mark `datetime` library as -passthrough (means you lose protection against calling the non-deterministic `now()`), or use non-compiled Pydantic -dependency. ### Activities @@ -1341,7 +1349,7 @@ async def check_past_histories(my_client: Client): OpenTelemetry support requires the optional `opentelemetry` dependencies which are part of the `opentelemetry` extra. When using `pip`, running - pip install temporalio[opentelemetry] + pip install 'temporalio[opentelemetry]' will install needed dependencies. Then the `temporalio.contrib.opentelemetry.TracingInterceptor` can be created and set as an interceptor on the `interceptors` argument of `Client.connect`. When set, spans will be created for all client diff --git a/poetry.lock b/poetry.lock index efa403d5..6b11c570 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,17 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"pydantic\"" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] [[package]] name = "attrs" @@ -6,6 +19,7 @@ version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, @@ -25,6 +39,7 @@ version = "24.8.1" description = "Self-service finite-state machines for the programmer on the go." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a"}, {file = "automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88"}, @@ -42,6 +57,7 @@ version = "0.16" description = "Python parser for bash" optional = false python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4" +groups = ["dev"] files = [ {file = "bashlex-0.16-py2.py3-none-any.whl", hash = "sha256:ff89fc743ccdef978792784d74d698a9236a862939bb4af471c0c3faf92c21bb"}, {file = "bashlex-0.16.tar.gz", hash = "sha256:dc6f017e49ce2d0fe30ad9f5206da9cd13ded073d365688c9fda525354e8c373"}, @@ -53,6 +69,7 @@ version = "5.0.1" description = "An easy safelist-based HTML-sanitizing tool." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, @@ -72,6 +89,7 @@ version = "2.3.post1" description = "Bash style brace expander." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "bracex-2.3.post1-py3-none-any.whl", hash = "sha256:351b7f20d56fb9ea91f9b9e9e7664db466eb234188c175fd943f8f755c807e73"}, {file = "bracex-2.3.post1.tar.gz", hash = "sha256:e7b23fc8b2cd06d3dec0692baabecb249dda94e06a617901ff03a6c56fd71693"}, @@ -83,6 +101,7 @@ version = "0.14.1" description = "httplib2 caching for requests" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, @@ -104,6 +123,7 @@ version = "2022.9.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"}, {file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"}, @@ -115,6 +135,8 @@ version = "1.15.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"linux\"" files = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, @@ -191,6 +213,7 @@ version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.6.0" +groups = ["dev"] files = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, @@ -205,6 +228,7 @@ version = "2.22.0" description = "Build Python wheels on CI with minimal configuration." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cibuildwheel-2.22.0-py3-none-any.whl", hash = "sha256:c40bb7ac7b57fed8195fca624cc9bd68334375d32b75bea6fa8330ac1cd902c4"}, {file = "cibuildwheel-2.22.0.tar.gz", hash = "sha256:6651e775ac26a86a49d67639aa3540f19728caf0dfcd80f156ba4f241aad4940"}, @@ -230,6 +254,8 @@ version = "0.4.5" description = "Cross-platform colored terminal text." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, @@ -241,6 +267,7 @@ version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, @@ -255,6 +282,7 @@ version = "1.5.3" description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "ConfigArgParse-1.5.3-py3-none-any.whl", hash = "sha256:18f6535a2db9f6e02bd5626cc7455eac3e96b9ab3d969d366f9aafd5c5c00fe7"}, {file = "ConfigArgParse-1.5.3.tar.gz", hash = "sha256:1b0b3cbf664ab59dada57123c81eff3d9737e0d11d8cf79e3d6eb10823f1739f"}, @@ -270,6 +298,7 @@ version = "15.1.0" description = "Symbolic constants in Python" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, @@ -281,6 +310,8 @@ version = "38.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "sys_platform == \"linux\"" files = [ {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, @@ -327,6 +358,7 @@ version = "1.3.0" description = "A tool for resolving PEP 735 Dependency Group data" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "dependency_groups-1.3.0-py3-none-any.whl", hash = "sha256:1abf34d712deda5581e80d507512664d52b35d1c2d7caf16c85e58ca508547e0"}, {file = "dependency_groups-1.3.0.tar.gz", hash = "sha256:5b9751d5d98fbd6dfd038a560a69c8382e41afcbf7ffdbcc28a2a3f85498830f"}, @@ -345,6 +377,8 @@ version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "extra == \"opentelemetry\"" files = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, @@ -362,6 +396,7 @@ version = "0.19" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, @@ -373,6 +408,8 @@ version = "1.1.3" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, @@ -387,6 +424,7 @@ version = "3.8.0" description = "A platform independent file lock." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, @@ -402,6 +440,7 @@ version = "1.68.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "grpcio-1.68.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:619b5d0f29f4f5351440e9343224c3e19912c21aeda44e0c49d0d147a8d01544"}, {file = "grpcio-1.68.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:a59f5822f9459bed098ffbceb2713abbf7c6fd13f2b9243461da5c338d0cd6c3"}, @@ -459,6 +498,7 @@ files = [ {file = "grpcio-1.68.0-cp39-cp39-win_amd64.whl", hash = "sha256:e694b5928b7b33ca2d3b4d5f9bf8b5888906f181daff6b406f4938f3a997a490"}, {file = "grpcio-1.68.0.tar.gz", hash = "sha256:7e7483d39b4a4fddb9906671e9ea21aaad4f031cdfc349fec76bdfa1e404543a"}, ] +markers = {main = "extra == \"grpc\""} [package.extras] protobuf = ["grpcio-tools (>=1.68.0)"] @@ -469,6 +509,7 @@ version = "1.68.0" description = "Protobuf code generator for gRPC" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "grpcio_tools-1.68.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:9509a5c3ed3d54fa7ac20748d501cb86668f764605a0a68f275339ee0f1dc1a6"}, {file = "grpcio_tools-1.68.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:59a885091bf29700ba0e14a954d156a18714caaa2006a7f328b18e1ac4b1e721"}, @@ -538,6 +579,7 @@ version = "21.0.0" description = "A featureful, immutable, and correct URL for Python." optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, @@ -552,6 +594,7 @@ version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -563,6 +606,7 @@ version = "4.12.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, @@ -582,6 +626,7 @@ version = "24.7.2" description = "A small library that versions your Python projects." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, @@ -600,6 +645,7 @@ version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -611,6 +657,7 @@ version = "3.2.2" description = "Utility functions for Python class constructs" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "jaraco.classes-3.2.2-py3-none-any.whl", hash = "sha256:e6ef6fd3fcf4579a7a019d87d1e56a883f4e4c35cfe925f86731abc58804e647"}, {file = "jaraco.classes-3.2.2.tar.gz", hash = "sha256:6745f113b0b588239ceb49532aa09c3ebb947433ce311ef2f8e3ad64ebb74594"}, @@ -629,6 +676,8 @@ version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "sys_platform == \"linux\"" files = [ {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, @@ -644,6 +693,7 @@ version = "23.9.1" description = "Store and access your passwords safely." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "keyring-23.9.1-py3-none-any.whl", hash = "sha256:3565b9e4ea004c96e158d2d332a49f466733d565bb24157a60fd2e49f41a0fd1"}, {file = "keyring-23.9.1.tar.gz", hash = "sha256:39e4f6572238d2615a82fcaa485e608b84b503cf080dc924c43bbbacb11c1c18"}, @@ -666,6 +716,7 @@ version = "0.6.2" description = "A Python implementation of Lunr.js" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "lunr-0.6.2-py2.py3-none-any.whl", hash = "sha256:6fbf619e07ff97c6198f9cae0fc32d69a0d49fb0708c2559efbede45670cba06"}, {file = "lunr-0.6.2.tar.gz", hash = "sha256:7983d965bd7baa78cbd4f5b934fc3ef3142c1b6089df6c6fecd66df5bff20921"}, @@ -680,6 +731,7 @@ version = "8.14.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, @@ -691,6 +743,7 @@ version = "1.0.4" description = "MessagePack serializer" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, @@ -752,6 +805,7 @@ version = "1.4.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, @@ -798,6 +852,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -809,6 +864,7 @@ version = "3.3.0" description = "Generate mypy stub files from protobuf specs" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mypy-protobuf-3.3.0.tar.gz", hash = "sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248"}, {file = "mypy_protobuf-3.3.0-py3-none-any.whl", hash = "sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d"}, @@ -824,6 +880,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -835,6 +892,8 @@ version = "1.12.0" description = "OpenTelemetry Python API" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"opentelemetry\"" files = [ {file = "opentelemetry-api-1.12.0.tar.gz", hash = "sha256:740c2cf9aa75e76c208b3ee04b3b3b3721f58bbac8e97019174f07ec12cde7af"}, {file = "opentelemetry_api-1.12.0-py3-none-any.whl", hash = "sha256:2e1cef8ce175be6464f240422babfe1dfb581daec96f0daad5d0d0e951b38f7b"}, @@ -850,6 +909,8 @@ version = "1.12.0" description = "OpenTelemetry Python SDK" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"opentelemetry\"" files = [ {file = "opentelemetry-sdk-1.12.0.tar.gz", hash = "sha256:bf37830ca4f93d0910cf109749237c5cb4465e31a54dfad8400011e9822a2a14"}, {file = "opentelemetry_sdk-1.12.0-py3-none-any.whl", hash = "sha256:d13be09765441c0513a3de01b7a2f56a7da36d902f60bff7c97f338903a57c34"}, @@ -867,6 +928,8 @@ version = "0.33b0" description = "OpenTelemetry Semantic Conventions" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"opentelemetry\"" files = [ {file = "opentelemetry-semantic-conventions-0.33b0.tar.gz", hash = "sha256:67d62461c87b683b958428ced79162ec4d567dabf30b050f270bbd01eff89ced"}, {file = "opentelemetry_semantic_conventions-0.33b0-py3-none-any.whl", hash = "sha256:56b67b3f8f49413cbfbbeb32e9cf7b4c7dfb27a83064d959733766376ba11bc7"}, @@ -878,6 +941,7 @@ version = "23.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, @@ -889,6 +953,7 @@ version = "1.8.3" description = "Query metadatdata from sdists / bdists / installed packages." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["dev"] files = [ {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, {file = "pkginfo-1.8.3.tar.gz", hash = "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c"}, @@ -903,6 +968,7 @@ version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, @@ -918,6 +984,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -933,6 +1000,7 @@ version = "5.28.3" description = "" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24"}, {file = "protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868"}, @@ -953,6 +1021,7 @@ version = "5.9.3" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "psutil-5.9.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b4a247cd3feaae39bb6085fcebf35b3b8ecd9b022db796d89c8f05067ca28e71"}, {file = "psutil-5.9.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5fa88e3d5d0b480602553d362c4b33a63e0c40bfea7312a7bf78799e01e0810b"}, @@ -1001,6 +1070,8 @@ version = "2.21" description = "C parser in Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "sys_platform == \"linux\"" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -1008,62 +1079,139 @@ files = [ [[package]] name = "pydantic" -version = "1.10.19" -description = "Data validation and settings management using python type hints" -optional = false -python-versions = ">=3.7" +version = "2.10.6" +description = "Data validation using Python type hints" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"pydantic\"" files = [ - {file = "pydantic-1.10.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a415b9e95fa602b10808113967f72b2da8722061265d6af69268c111c254832d"}, - {file = "pydantic-1.10.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:11965f421f7eb026439d4eb7464e9182fe6d69c3d4d416e464a4485d1ba61ab6"}, - {file = "pydantic-1.10.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bb81fcfc6d5bff62cd786cbd87480a11d23f16d5376ad2e057c02b3b44df96"}, - {file = "pydantic-1.10.19-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ee8c9916689f8e6e7d90161e6663ac876be2efd32f61fdcfa3a15e87d4e413"}, - {file = "pydantic-1.10.19-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0399094464ae7f28482de22383e667625e38e1516d6b213176df1acdd0c477ea"}, - {file = "pydantic-1.10.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8b2cf5e26da84f2d2dee3f60a3f1782adedcee785567a19b68d0af7e1534bd1f"}, - {file = "pydantic-1.10.19-cp310-cp310-win_amd64.whl", hash = "sha256:1fc8cc264afaf47ae6a9bcbd36c018d0c6b89293835d7fb0e5e1a95898062d59"}, - {file = "pydantic-1.10.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7a8a1dd68bac29f08f0a3147de1885f4dccec35d4ea926e6e637fac03cdb4b3"}, - {file = "pydantic-1.10.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07d00ca5ef0de65dd274005433ce2bb623730271d495a7d190a91c19c5679d34"}, - {file = "pydantic-1.10.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad57004e5d73aee36f1e25e4e73a4bc853b473a1c30f652dc8d86b0a987ffce3"}, - {file = "pydantic-1.10.19-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dce355fe7ae53e3090f7f5fa242423c3a7b53260747aa398b4b3aaf8b25f41c3"}, - {file = "pydantic-1.10.19-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0d32227ea9a3bf537a2273fd2fdb6d64ab4d9b83acd9e4e09310a777baaabb98"}, - {file = "pydantic-1.10.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e351df83d1c9cffa53d4e779009a093be70f1d5c6bb7068584086f6a19042526"}, - {file = "pydantic-1.10.19-cp311-cp311-win_amd64.whl", hash = "sha256:d8d72553d2f3f57ce547de4fa7dc8e3859927784ab2c88343f1fc1360ff17a08"}, - {file = "pydantic-1.10.19-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d5b5b7c6bafaef90cbb7dafcb225b763edd71d9e22489647ee7df49d6d341890"}, - {file = "pydantic-1.10.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:570ad0aeaf98b5e33ff41af75aba2ef6604ee25ce0431ecd734a28e74a208555"}, - {file = "pydantic-1.10.19-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0890fbd7fec9e151c7512941243d830b2d6076d5df159a2030952d480ab80a4e"}, - {file = "pydantic-1.10.19-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec5c44e6e9eac5128a9bfd21610df3b8c6b17343285cc185105686888dc81206"}, - {file = "pydantic-1.10.19-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6eb56074b11a696e0b66c7181da682e88c00e5cebe6570af8013fcae5e63e186"}, - {file = "pydantic-1.10.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9d7d48fbc5289efd23982a0d68e973a1f37d49064ccd36d86de4543aff21e086"}, - {file = "pydantic-1.10.19-cp312-cp312-win_amd64.whl", hash = "sha256:fd34012691fbd4e67bdf4accb1f0682342101015b78327eaae3543583fcd451e"}, - {file = "pydantic-1.10.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a5d5b877c7d3d9e17399571a8ab042081d22fe6904416a8b20f8af5909e6c8f"}, - {file = "pydantic-1.10.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c46f58ef2df958ed2ea7437a8be0897d5efe9ee480818405338c7da88186fb3"}, - {file = "pydantic-1.10.19-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d8a38a44bb6a15810084316ed69c854a7c06e0c99c5429f1d664ad52cec353c"}, - {file = "pydantic-1.10.19-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a82746c6d6e91ca17e75f7f333ed41d70fce93af520a8437821dec3ee52dfb10"}, - {file = "pydantic-1.10.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:566bebdbe6bc0ac593fa0f67d62febbad9f8be5433f686dc56401ba4aab034e3"}, - {file = "pydantic-1.10.19-cp37-cp37m-win_amd64.whl", hash = "sha256:22a1794e01591884741be56c6fba157c4e99dcc9244beb5a87bd4aa54b84ea8b"}, - {file = "pydantic-1.10.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:076c49e24b73d346c45f9282d00dbfc16eef7ae27c970583d499f11110d9e5b0"}, - {file = "pydantic-1.10.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d4320510682d5a6c88766b2a286d03b87bd3562bf8d78c73d63bab04b21e7b4"}, - {file = "pydantic-1.10.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e66aa0fa7f8aa9d0a620361834f6eb60d01d3e9cea23ca1a92cda99e6f61dac"}, - {file = "pydantic-1.10.19-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d216f8d0484d88ab72ab45d699ac669fe031275e3fa6553e3804e69485449fa0"}, - {file = "pydantic-1.10.19-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f28a81978e936136c44e6a70c65bde7548d87f3807260f73aeffbf76fb94c2f"}, - {file = "pydantic-1.10.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d3449633c207ec3d2d672eedb3edbe753e29bd4e22d2e42a37a2c1406564c20f"}, - {file = "pydantic-1.10.19-cp38-cp38-win_amd64.whl", hash = "sha256:7ea24e8614f541d69ea72759ff635df0e612b7dc9d264d43f51364df310081a3"}, - {file = "pydantic-1.10.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:573254d844f3e64093f72fcd922561d9c5696821ff0900a0db989d8c06ab0c25"}, - {file = "pydantic-1.10.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff09600cebe957ecbb4a27496fe34c1d449e7957ed20a202d5029a71a8af2e35"}, - {file = "pydantic-1.10.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4739c206bfb6bb2bdc78dcd40bfcebb2361add4ceac6d170e741bb914e9eff0f"}, - {file = "pydantic-1.10.19-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfb5b378b78229119d66ced6adac2e933c67a0aa1d0a7adffbe432f3ec14ce4"}, - {file = "pydantic-1.10.19-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f31742c95e3f9443b8c6fa07c119623e61d76603be9c0d390bcf7e888acabcb"}, - {file = "pydantic-1.10.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c6444368b651a14c2ce2fb22145e1496f7ab23cbdb978590d47c8d34a7bc0289"}, - {file = "pydantic-1.10.19-cp39-cp39-win_amd64.whl", hash = "sha256:945407f4d08cd12485757a281fca0e5b41408606228612f421aa4ea1b63a095d"}, - {file = "pydantic-1.10.19-py3-none-any.whl", hash = "sha256:2206a1752d9fac011e95ca83926a269fb0ef5536f7e053966d058316e24d929f"}, - {file = "pydantic-1.10.19.tar.gz", hash = "sha256:fea36c2065b7a1d28c6819cc2e93387b43dd5d3cf5a1e82d8132ee23f36d1f10"}, + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"pydantic\"" +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydocstyle" @@ -1071,6 +1219,7 @@ version = "6.3.0" description = "Python docstring style checker" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, @@ -1088,6 +1237,7 @@ version = "24.11.1" description = "API doc generator." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pydoctor-24.11.1-py3-none-any.whl", hash = "sha256:eb180c1a784380bd5fcfa2f3b190608eb8be6d8a3647f8e382ec78a53b5248d5"}, {file = "pydoctor-24.11.1.tar.gz", hash = "sha256:5e02aba4d15d3f7c4e1509aa9e2bf0abf1b5912721ac434b0cec0ec7909f4f83"}, @@ -1117,6 +1267,7 @@ version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, @@ -1131,6 +1282,7 @@ version = "1.1.377" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162"}, {file = "pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf"}, @@ -1149,6 +1301,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -1171,6 +1324,7 @@ version = "0.21.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, @@ -1189,6 +1343,7 @@ version = "2.2.0" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-timeout-2.2.0.tar.gz", hash = "sha256:3b0b95dabf3cb50bac9ef5ca912fa0cfc286526af17afc806824df20c2f72c90"}, {file = "pytest_timeout-2.2.0-py3-none-any.whl", hash = "sha256:bde531e096466f49398a59f2dde76fa78429a09a12411466f88a07213e220de2"}, @@ -1203,6 +1358,8 @@ version = "2.8.2" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version < \"3.11\"" files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -1217,6 +1374,8 @@ version = "0.2.0" description = "" optional = false python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, @@ -1228,6 +1387,7 @@ version = "37.1" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "readme_renderer-37.1-py3-none-any.whl", hash = "sha256:16c914ca7731fd062a316a2a8e5434a175ee34661a608af771a60c881f528a34"}, {file = "readme_renderer-37.1.tar.gz", hash = "sha256:96768c069729f69176f514477e57f2f8cd543fbb2cd7bad372976249fa509a0c"}, @@ -1247,6 +1407,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1268,6 +1429,7 @@ version = "0.9.1" description = "A utility belt for advanced users of python-requests" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, @@ -1282,6 +1444,7 @@ version = "2.0.0" description = "Validating URI References per RFC 3986" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, @@ -1296,6 +1459,7 @@ version = "12.5.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.6.3,<4.0.0" +groups = ["dev"] files = [ {file = "rich-12.5.1-py3-none-any.whl", hash = "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb"}, {file = "rich-12.5.1.tar.gz", hash = "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca"}, @@ -1314,6 +1478,7 @@ version = "0.5.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, @@ -1341,6 +1506,8 @@ version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "sys_platform == \"linux\"" files = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, @@ -1356,6 +1523,7 @@ version = "2.10.0" description = "A library implementing the 'SemVer' scheme." optional = false python-versions = ">=2.7" +groups = ["dev"] files = [ {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, @@ -1371,6 +1539,7 @@ version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, @@ -1387,6 +1556,7 @@ version = "1.5.2" description = "Setuptools Rust extension plugin" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "setuptools-rust-1.5.2.tar.gz", hash = "sha256:d8daccb14dc0eae1b6b6eb3ecef79675bd37b4065369f79c35393dd5c55652c7"}, {file = "setuptools_rust-1.5.2-py3-none-any.whl", hash = "sha256:8eb45851e34288f2296cd5ab9e924535ac1757318b730a13fe6836867843f206"}, @@ -1403,10 +1573,12 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +markers = {main = "python_version < \"3.11\""} [[package]] name = "snowballstemmer" @@ -1414,6 +1586,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -1425,6 +1598,7 @@ version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1436,6 +1610,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1447,6 +1623,7 @@ version = "4.0.1" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "twine-4.0.1-py3-none-any.whl", hash = "sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e"}, {file = "twine-4.0.1.tar.gz", hash = "sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0"}, @@ -1469,6 +1646,7 @@ version = "24.11.0" description = "An asynchronous networking framework written in Python" optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "twisted-24.11.0-py3-none-any.whl", hash = "sha256:fe403076c71f04d5d2d789a755b687c5637ec3bcd3b2b8252d76f2ba65f54261"}, {file = "twisted-24.11.0.tar.gz", hash = "sha256:695d0556d5ec579dcc464d2856b634880ed1319f45b10d19043f2b57eb0115b5"}, @@ -1504,6 +1682,7 @@ version = "4.21.0.6" description = "Typing stubs for protobuf" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "types-protobuf-4.21.0.6.tar.gz", hash = "sha256:8c105b906569e9d53ba033465880d9ef17a59bf3ba8ab656d24c9eadb9d8a056"}, {file = "types_protobuf-4.21.0.6-py3-none-any.whl", hash = "sha256:39167012ead0bc5920b6322a1e4dc2d088f66a34b84cce39bb88500e49ac955a"}, @@ -1515,6 +1694,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1526,6 +1706,7 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, @@ -1543,6 +1724,7 @@ version = "0.5.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, @@ -1554,6 +1736,7 @@ version = "0.42.0" description = "A built-package format for Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "wheel-0.42.0-py3-none-any.whl", hash = "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d"}, {file = "wheel-0.42.0.tar.gz", hash = "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"}, @@ -1568,6 +1751,8 @@ version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +groups = ["main"] +markers = "extra == \"opentelemetry\"" files = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, @@ -1651,6 +1836,7 @@ version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, @@ -1666,6 +1852,7 @@ version = "7.2" description = "Interfaces for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, @@ -1717,8 +1904,9 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [extras] grpc = ["grpcio"] opentelemetry = ["opentelemetry-api", "opentelemetry-sdk"] +pydantic = ["pydantic"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.9" -content-hash = "63c0f8bf444280248b4d756f5baadd98b663968b8ec72a92150e0009000f0638" +content-hash = "fe88ba77a85c62862831e8286dddfc0530d1b7ad3c5c38be31f1508fd496c6e2" diff --git a/pyproject.toml b/pyproject.toml index ee3b6cec..bf2a7b2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ grpcio = {version = "^1.48.2", optional = true} opentelemetry-api = { version = "^1.11.1", optional = true } opentelemetry-sdk = { version = "^1.11.1", optional = true } protobuf = ">=3.20" +pydantic = { version = "^2.0.0", optional = true } python = "^3.9" python-dateutil = { version = "^2.8.2", python = "<3.11" } types-protobuf = ">=3.20" @@ -46,7 +47,6 @@ grpcio-tools = "^1.48.2" mypy = "^1.0.0" mypy-protobuf = "^3.3.0" psutil = "^5.9.3" -pydantic = "^1.10.19" pydocstyle = "^6.3.0" pydoctor = "^24.11.1" pyright = ">=1.1.377" @@ -61,8 +61,9 @@ twine = "^4.0.1" wheel = "^0.42.0" [tool.poetry.extras] -opentelemetry = ["opentelemetry-api", "opentelemetry-sdk"] grpc = ["grpcio"] +opentelemetry = ["opentelemetry-api", "opentelemetry-sdk"] +pydantic = ["pydantic"] [tool.poetry.group.dev.dependencies] ruff = "^0.5.0" diff --git a/temporalio/contrib/pydantic.py b/temporalio/contrib/pydantic.py new file mode 100644 index 00000000..2e78ac5d --- /dev/null +++ b/temporalio/contrib/pydantic.py @@ -0,0 +1,114 @@ +"""A data converter for Pydantic v2. + +To use, pass ``pydantic_data_converter`` as the ``data_converter`` argument to +:py:class:`temporalio.client.Client`: + +.. code-block:: python + + client = Client( + data_converter=pydantic_data_converter, + ... + ) + +Pydantic v1 is not supported. +""" + +from typing import Any, Optional, Type + +from pydantic import TypeAdapter +from pydantic_core import to_json + +import temporalio.api.common.v1 +from temporalio.converter import ( + CompositePayloadConverter, + DataConverter, + DefaultPayloadConverter, + EncodingPayloadConverter, + JSONPlainPayloadConverter, +) + +# Note that in addition to the implementation in this module, _RestrictedProxy +# implements __get_pydantic_core_schema__ so that pydantic unwraps proxied types. + + +class PydanticJSONPlainPayloadConverter(EncodingPayloadConverter): + """Pydantic JSON payload converter. + + Supports conversion of all types supported by Pydantic to and from JSON. + + In addition to Pydantic models, these include all `json.dump`-able types, + various non-`json.dump`-able standard library types such as dataclasses, + types from the datetime module, sets, UUID, etc, and custom types composed + of any of these. + + See https://docs.pydantic.dev/latest/api/standard_library_types/ + """ + + @property + def encoding(self) -> str: + """See base class.""" + return "json/plain" + + def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: + """See base class. + + Uses ``pydantic_core.to_json`` to serialize ``value`` to JSON. + + See + https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.to_json. + """ + return temporalio.api.common.v1.Payload( + metadata={"encoding": self.encoding.encode()}, data=to_json(value) + ) + + def from_payload( + self, + payload: temporalio.api.common.v1.Payload, + type_hint: Optional[Type] = None, + ) -> Any: + """See base class. + + Uses ``pydantic.TypeAdapter.validate_json`` to construct an + instance of the type specified by ``type_hint`` from the JSON payload. + + See + https://docs.pydantic.dev/latest/api/type_adapter/#pydantic.type_adapter.TypeAdapter.validate_json. + """ + _type_hint = type_hint if type_hint is not None else Any + return TypeAdapter(_type_hint).validate_json(payload.data) + + +class PydanticPayloadConverter(CompositePayloadConverter): + """Payload converter for payloads containing pydantic model instances. + + JSON conversion is replaced with a converter that uses + :py:class:`PydanticJSONPlainPayloadConverter`. + """ + + def __init__(self) -> None: + """Initialize object""" + json_payload_converter = PydanticJSONPlainPayloadConverter() + super().__init__( + *( + c + if not isinstance(c, JSONPlainPayloadConverter) + else json_payload_converter + for c in DefaultPayloadConverter.default_encoding_payload_converters + ) + ) + + +pydantic_data_converter = DataConverter( + payload_converter_class=PydanticPayloadConverter +) +"""Pydantic data converter. + +Supports conversion of all types supported by Pydantic to and from JSON. + +In addition to Pydantic models, these include all `json.dump`-able types, +various non-`json.dump`-able standard library types such as dataclasses, +types from the datetime module, sets, UUID, etc, and custom types composed +of any of these. + +To use, pass as the ``data_converter`` argument of :py:class:`temporalio.client.Client` +""" diff --git a/temporalio/converter.py b/temporalio/converter.py index 61037dc3..e52a344b 100644 --- a/temporalio/converter.py +++ b/temporalio/converter.py @@ -15,6 +15,7 @@ from dataclasses import dataclass from datetime import datetime from enum import IntEnum +from itertools import zip_longest from typing import ( Any, Awaitable, @@ -291,10 +292,8 @@ def from_payloads( RuntimeError: Error during decode """ values = [] - for index, payload in enumerate(payloads): - type_hint = None - if type_hints and len(type_hints) > index: - type_hint = type_hints[index] + type_hints = type_hints or [] + for index, (payload, type_hint) in enumerate(zip_longest(payloads, type_hints)): # Raw value should just wrap if type_hint == temporalio.common.RawValue: values.append(temporalio.common.RawValue(payload)) @@ -488,8 +487,11 @@ def from_payload( class AdvancedJSONEncoder(json.JSONEncoder): """Advanced JSON encoder. - This encoder supports dataclasses, classes with dict() functions, and - all iterables as lists. + This encoder supports dataclasses and all iterables as lists. + + It also uses Pydantic v1's "dict" methods if available on the object, + but this is deprecated. Pydantic users should upgrade to v2 and use + temporalio.contrib.pydantic.pydantic_data_converter. """ def default(self, o: Any) -> Any: @@ -500,7 +502,7 @@ def default(self, o: Any) -> Any: # Dataclass support if dataclasses.is_dataclass(o): return dataclasses.asdict(o) - # Support for models with "dict" function like Pydantic + # Support for Pydantic v1's dict method dict_fn = getattr(o, "dict", None) if callable(dict_fn): return dict_fn() @@ -558,10 +560,11 @@ def encoding(self) -> str: def to_payload(self, value: Any) -> Optional[temporalio.api.common.v1.Payload]: """See base class.""" - # Check for pydantic then send warning + # Check for Pydantic v1 if hasattr(value, "parse_obj"): warnings.warn( - "If you're using pydantic model, refer to https://github.com/temporalio/samples-python/tree/main/pydantic_converter for better support" + "If you're using Pydantic v2, use temporalio.contrib.pydantic.pydantic_data_converter. " + "If you're using Pydantic v1 and cannot upgrade, refer to https://github.com/temporalio/samples-python/tree/main/pydantic_converter_v1 for better v1 support." ) # We let JSON conversion errors be thrown to caller return temporalio.api.common.v1.Payload( @@ -1523,8 +1526,12 @@ def value_to_type( # TODO(cretz): Want way to convert snake case to camel case? return hint(**field_values) - # If there is a @staticmethod or @classmethod parse_obj, we will use it. - # This covers Pydantic models. + # Pydantic model instance + # Pydantic users should use Pydantic v2 with + # temporalio.contrib.pydantic.pydantic_data_converter, in which case a + # pydantic model instance will have been handled by the custom_converters at + # the start of this function. We retain the following for backwards + # compatibility with pydantic v1 users, but this is deprecated. parse_obj_attr = inspect.getattr_static(hint, "parse_obj", None) if isinstance(parse_obj_attr, classmethod) or isinstance( parse_obj_attr, staticmethod diff --git a/temporalio/worker/workflow_sandbox/_restrictions.py b/temporalio/worker/workflow_sandbox/_restrictions.py index 407e51b2..3796fd7a 100644 --- a/temporalio/worker/workflow_sandbox/_restrictions.py +++ b/temporalio/worker/workflow_sandbox/_restrictions.py @@ -7,6 +7,7 @@ from __future__ import annotations import dataclasses +import datetime import functools import inspect import logging @@ -31,6 +32,14 @@ cast, ) +try: + import pydantic + import pydantic_core + + HAVE_PYDANTIC = True +except ImportError: + HAVE_PYDANTIC = False + import temporalio.workflow logger = logging.getLogger(__name__) @@ -435,9 +444,11 @@ def with_child_unrestricted(self, *child_path: str) -> SandboxMatcher: # Due to a metaclass conflict in sandbox, we need zipfile module to pass # through always "zipfile", - # This is a very general module needed by many things including pytest's + # Very general modules needed by many things including pytest's # assertion rewriter "typing", + # Required for Pydantic TypedDict fields. + "typing_extensions", # Required due to https://github.com/protocolbuffers/protobuf/issues/10143 # for older versions. This unfortunately means that on those versions, # everyone using Python protos has to pass their module through. @@ -943,7 +954,17 @@ def r_op(obj: Any, other: Any) -> Any: def _is_restrictable(v: Any) -> bool: return v is not None and not isinstance( - v, (bool, int, float, complex, str, bytes, bytearray) + v, + ( + bool, + int, + float, + complex, + str, + bytes, + bytearray, + datetime.date, # e.g. datetime.datetime + ), ) @@ -971,6 +992,8 @@ def __init__(self, *args, **kwargs) -> None: _trace("__init__ unrecognized with args %s", args) def __getattribute__(self, __name: str) -> Any: + if HAVE_PYDANTIC and __name == "__get_pydantic_core_schema__": + return object.__getattribute__(self, "__get_pydantic_core_schema__") state = _RestrictionState.from_proxy(self) _trace("__getattribute__ %s on %s", __name, state.name) # We do not restrict __spec__ or __name__ @@ -1020,6 +1043,17 @@ def __getitem__(self, key: Any) -> Any: ) return ret + if HAVE_PYDANTIC: + # Instruct pydantic to use the proxied type when determining the schema + # https://docs.pydantic.dev/latest/concepts/types/#customizing-validation-with-__get_pydantic_core_schema__ + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: pydantic.GetCoreSchemaHandler, + ) -> pydantic_core.CoreSchema: + return handler(RestrictionContext.unwrap_if_proxied(source_type)) + __doc__ = _RestrictedProxyLookup( # type: ignore class_value=__doc__, fallback_func=lambda self: type(self).__doc__, is_attr=True ) @@ -1032,7 +1066,7 @@ def __getitem__(self, key: Any) -> Any: ) __str__ = _RestrictedProxyLookup(str) # type: ignore __bytes__ = _RestrictedProxyLookup(bytes) - __format__ = _RestrictedProxyLookup() # type: ignore + __format__ = _RestrictedProxyLookup(format) # type: ignore __lt__ = _RestrictedProxyLookup(operator.lt) __le__ = _RestrictedProxyLookup(operator.le) __eq__ = _RestrictedProxyLookup(operator.eq) # type: ignore diff --git a/tests/contrib/pydantic/activities.py b/tests/contrib/pydantic/activities.py new file mode 100644 index 00000000..ad1b8650 --- /dev/null +++ b/tests/contrib/pydantic/activities.py @@ -0,0 +1,36 @@ +from datetime import datetime +from typing import List +from uuid import UUID + +from temporalio import activity +from tests.contrib.pydantic.models import PydanticModels + + +@activity.defn +async def pydantic_objects_activity( + models: List[PydanticModels], +) -> List[PydanticModels]: + return models + + +@activity.defn +async def misc_objects_activity( + models: tuple[ + int, + str, + dict[str, float], + list[dict[str, float]], + tuple[dict[str, float]], + datetime, + UUID, + ], +) -> tuple[ + int, + str, + dict[str, float], + list[dict[str, float]], + tuple[dict[str, float]], + datetime, + UUID, +]: + return models diff --git a/tests/contrib/pydantic/models.py b/tests/contrib/pydantic/models.py new file mode 100644 index 00000000..e2fdcfe2 --- /dev/null +++ b/tests/contrib/pydantic/models.py @@ -0,0 +1,426 @@ +import dataclasses +import uuid +from datetime import date, datetime, time, timedelta, timezone +from ipaddress import IPv4Address +from pathlib import Path +from typing import ( + Annotated, + Any, + Dict, + Generic, + List, + Sequence, + Tuple, + TypeVar, + Union, + cast, +) + +from annotated_types import Len +from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema + +from temporalio import workflow + +# Define some of the models outside the sandbox +with workflow.unsafe.imports_passed_through(): + from tests.contrib.pydantic.models_2 import ( + ComplexTypesModel, + StandardTypesModel, + StrictStandardTypesModel, + make_complex_types_object, + make_standard_types_object, + make_strict_standard_types_object, + ) + +SequenceType = TypeVar("SequenceType", bound=Sequence[Any]) +ShortSequence = Annotated[SequenceType, Len(max_length=2)] + + +class SpecialTypesModel(BaseModel): + datetime_field: datetime + datetime_field_int: datetime + datetime_field_float: datetime + datetime_field_str_formatted: datetime + datetime_field_str_int: datetime + datetime_field_date: datetime + + time_field: time + time_field_str: time + + date_field: date + timedelta_field: timedelta + path_field: Path + uuid_field: uuid.UUID + ip_field: IPv4Address + + def _check_instance(self) -> None: + dt = datetime(2000, 1, 2, 3, 4, 5) + dtz = datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + assert isinstance(self.datetime_field, datetime) + assert isinstance(self.datetime_field_int, datetime) + assert isinstance(self.datetime_field_float, datetime) + assert isinstance(self.datetime_field_str_formatted, datetime) + assert isinstance(self.datetime_field_str_int, datetime) + assert isinstance(self.datetime_field_date, datetime) + assert isinstance(self.timedelta_field, timedelta) + assert isinstance(self.path_field, Path) + assert isinstance(self.uuid_field, uuid.UUID) + assert isinstance(self.ip_field, IPv4Address) + assert self.datetime_field == dt + assert self.datetime_field_int == dtz + assert self.datetime_field_float == dtz + assert self.datetime_field_str_formatted == dtz + assert self.datetime_field_str_int == dtz + assert self.datetime_field_date == datetime(2000, 1, 2) + assert self.time_field == time(3, 4, 5) + assert self.time_field_str == time(3, 4, 5, tzinfo=timezone.utc) + assert self.date_field == date(2000, 1, 2) + assert self.timedelta_field == timedelta(days=1, hours=2) + assert self.path_field == Path("test/path") + assert self.uuid_field == uuid.UUID("12345678-1234-5678-1234-567812345678") + assert self.ip_field == IPv4Address("127.0.0.1") + + +def make_special_types_object() -> SpecialTypesModel: + return SpecialTypesModel( + datetime_field=datetime(2000, 1, 2, 3, 4, 5), + datetime_field_int=946782245, # type: ignore + datetime_field_float=946782245.0, # type: ignore + datetime_field_str_formatted="2000-01-02T03:04:05Z", # type: ignore + datetime_field_str_int="946782245", # type: ignore + datetime_field_date=datetime(2000, 1, 2), + time_field=time(3, 4, 5), + time_field_str="03:04:05Z", # type: ignore + date_field=date(2000, 1, 2), + timedelta_field=timedelta(days=1, hours=2), + path_field=Path("test/path"), + uuid_field=uuid.UUID("12345678-1234-5678-1234-567812345678"), + ip_field=IPv4Address("127.0.0.1"), + ) + + +class StrictSpecialTypesModel(SpecialTypesModel): + model_config = ConfigDict(strict=True) + + +def make_strict_special_types_object() -> StrictSpecialTypesModel: + return cast(StrictSpecialTypesModel, make_special_types_object()) + + +class ChildModel(BaseModel): + name: str + value: int + + +class ParentModel(BaseModel): + child: ChildModel + children: List[ChildModel] + + def _check_instance(self) -> None: + assert isinstance(self.child, ChildModel) + assert isinstance(self.children, list) + assert all(isinstance(child, ChildModel) for child in self.children) + assert self.child.name == "child1" + assert self.child.value == 1 + assert len(self.children) == 2 + assert self.children[0].name == "child2" + assert self.children[0].value == 2 + assert self.children[1].name == "child3" + assert self.children[1].value == 3 + + +def make_nested_object() -> ParentModel: + return ParentModel( + child=ChildModel(name="child1", value=1), + children=[ + ChildModel(name="child2", value=2), + ChildModel(name="child3", value=3), + ], + ) + + +class FieldFeaturesModel(BaseModel): + field_with_default: str = "default" + field_with_factory: datetime = Field( + default_factory=lambda: datetime(2000, 1, 2, 3, 4, 5) + ) + field_with_constraints: int = Field(gt=0, lt=100) + field_with_alias: str = Field(alias="different_name") + + def _check_instance(self) -> None: + assert isinstance(self.field_with_default, str) + assert isinstance(self.field_with_factory, datetime) + assert isinstance(self.field_with_constraints, int) + assert isinstance(self.field_with_alias, str) + assert self.field_with_default == "default" + assert 0 < self.field_with_constraints < 100 + assert self.field_with_alias == "aliased_value" + + +def make_field_features_object() -> FieldFeaturesModel: + return FieldFeaturesModel( + field_with_constraints=50, + different_name="aliased_value", + ) + + +class AnnotatedFieldsModel(BaseModel): + max_length_str: Annotated[str, Len(max_length=10)] + custom_json: Annotated[Dict[str, Any], WithJsonSchema({"extra": "data"})] + + def _check_instance(self) -> None: + assert isinstance(self.max_length_str, str) + assert isinstance(self.custom_json, dict) + assert len(self.max_length_str) <= 10 + assert self.max_length_str == "short" + assert self.custom_json == {"key": "value"} + + +def make_annotated_fields_object() -> AnnotatedFieldsModel: + return AnnotatedFieldsModel( + max_length_str="short", + custom_json={"key": "value"}, + ) + + +T = TypeVar("T") + + +class GenericModel(BaseModel, Generic[T]): + value: T + values: List[T] + + def _check_instance(self) -> None: + assert isinstance(self.value, str) + assert isinstance(self.values, list) + assert all(isinstance(v, str) for v in self.values) + assert self.value == "single" + assert self.values == ["multiple", "values"] + + +def make_generic_string_object() -> GenericModel[str]: + return GenericModel[str]( + value="single", + values=["multiple", "values"], + ) + + +class UnionModel(BaseModel): + simple_union_field: Union[str, int] + proxied_union_field: Union[datetime, Path] + + def _check_instance(self) -> None: + assert isinstance(self.simple_union_field, str) + assert self.simple_union_field == "string_or_int" + assert isinstance(self.proxied_union_field, Path) + assert self.proxied_union_field == Path("test/path") + + +def make_union_object() -> UnionModel: + return UnionModel( + simple_union_field="string_or_int", + proxied_union_field=Path("test/path"), + ) + + +class PydanticDatetimeModel(BaseModel): + datetime_field: datetime + datetime_field_assigned_field: datetime = Field() + datetime_field_with_default: datetime = Field( + default_factory=lambda: datetime(2000, 1, 2, 3, 4, 5) + ) + annotated_datetime: Annotated[datetime, Field(), WithJsonSchema({"extra": "data"})] + annotated_list_of_datetime: Annotated[ + List[datetime], Field(), WithJsonSchema({"extra": "data"}) + ] + datetime_short_sequence: ShortSequence[List[datetime]] + + def _check_instance(self): + _assert_datetime_validity(self.datetime_field) + _assert_datetime_validity(self.datetime_field_assigned_field) + _assert_datetime_validity(self.datetime_field_with_default) + _assert_datetime_validity(self.annotated_datetime) + assert isinstance(self.annotated_list_of_datetime, list) + assert isinstance(self.datetime_short_sequence, list) + assert self.annotated_datetime == datetime(2000, 1, 2, 3, 4, 5) + assert self.annotated_list_of_datetime == [ + datetime(2000, 1, 2, 3, 4, 5), + datetime(2001, 11, 12, 13, 14, 15), + ] + assert self.datetime_short_sequence == [ + datetime(2000, 1, 2, 3, 4, 5), + datetime(2001, 11, 12, 13, 14, 15), + ] + + +def make_pydantic_datetime_object() -> PydanticDatetimeModel: + return PydanticDatetimeModel( + datetime_field=datetime(2000, 1, 2, 3, 4, 5), + datetime_field_assigned_field=datetime(2000, 1, 2, 3, 4, 5), + annotated_datetime=datetime(2000, 1, 2, 3, 4, 5), + annotated_list_of_datetime=[ + datetime(2000, 1, 2, 3, 4, 5), + datetime(2001, 11, 12, 13, 14, 15), + ], + datetime_short_sequence=[ + datetime(2000, 1, 2, 3, 4, 5), + datetime(2001, 11, 12, 13, 14, 15), + ], + ) + + +class PydanticDateModel(BaseModel): + date_field: date + date_field_assigned_field: date = Field() + date_field_with_default: date = Field(default_factory=lambda: date(2000, 1, 2)) + annotated_date: Annotated[date, Field(), WithJsonSchema({"extra": "data"})] + annotated_list_of_date: Annotated[ + List[date], Field(), WithJsonSchema({"extra": "data"}) + ] + date_short_sequence: ShortSequence[List[date]] + + def _check_instance(self): + _assert_date_validity(self.date_field) + _assert_date_validity(self.date_field_assigned_field) + _assert_date_validity(self.date_field_with_default) + _assert_date_validity(self.annotated_date) + assert isinstance(self.annotated_list_of_date, list) + assert isinstance(self.date_short_sequence, list) + assert self.annotated_date == date(2000, 1, 2) + assert self.annotated_list_of_date == [ + date(2000, 1, 2), + date(2001, 11, 12), + ] + assert self.date_short_sequence == [ + date(2000, 1, 2), + date(2001, 11, 12), + ] + + +def make_pydantic_date_object() -> PydanticDateModel: + return PydanticDateModel( + date_field=date(2000, 1, 2), + date_field_assigned_field=date(2000, 1, 2), + annotated_date=date(2000, 1, 2), + annotated_list_of_date=[date(2000, 1, 2), date(2001, 11, 12)], + date_short_sequence=[date(2000, 1, 2), date(2001, 11, 12)], + ) + + +class PydanticTimedeltaModel(BaseModel): + timedelta_field: timedelta + timedelta_field_assigned_field: timedelta = Field() + timedelta_field_with_default: timedelta = Field( + default_factory=lambda: timedelta(days=1) + ) + annotated_timedelta: Annotated[ + timedelta, Field(), WithJsonSchema({"extra": "data"}) + ] + annotated_list_of_timedelta: Annotated[ + List[timedelta], Field(), WithJsonSchema({"extra": "data"}) + ] + timedelta_short_sequence: ShortSequence[List[timedelta]] + + def _check_instance(self): + _assert_timedelta_validity(self.timedelta_field) + _assert_timedelta_validity(self.timedelta_field_assigned_field) + _assert_timedelta_validity(self.timedelta_field_with_default) + _assert_timedelta_validity(self.annotated_timedelta) + assert isinstance(self.annotated_list_of_timedelta, list) + for td in self.annotated_list_of_timedelta: + _assert_timedelta_validity(td) + assert isinstance(self.timedelta_short_sequence, list) + for td in self.timedelta_short_sequence: + _assert_timedelta_validity(td) + assert self.annotated_timedelta == timedelta(1, 2, 3, 4, 5, 6, 7) + assert self.annotated_list_of_timedelta == [ + timedelta(1, 2, 3, 4, 5, 6, 7), + timedelta(2, 3, 4, 5, 6, 7, 8), + ] + + +def make_pydantic_timedelta_object() -> PydanticTimedeltaModel: + return PydanticTimedeltaModel( + timedelta_field=timedelta(1, 2, 3, 4, 5, 6, 7), + timedelta_field_assigned_field=timedelta(1, 2, 3, 4, 5, 6, 7), + annotated_timedelta=timedelta(1, 2, 3, 4, 5, 6, 7), + annotated_list_of_timedelta=[ + timedelta(1, 2, 3, 4, 5, 6, 7), + timedelta(2, 3, 4, 5, 6, 7, 8), + ], + timedelta_short_sequence=[ + timedelta(1, 2, 3, 4, 5, 6, 7), + timedelta(2, 3, 4, 5, 6, 7, 8), + ], + ) + + +def _assert_datetime_validity(dt: datetime): + assert isinstance(dt, datetime) + assert issubclass(dt.__class__, datetime) + + +def _assert_date_validity(d: date): + assert isinstance(d, date) + assert issubclass(d.__class__, date) + + +def _assert_timedelta_validity(td: timedelta): + assert isinstance(td, timedelta) + assert issubclass(td.__class__, timedelta) + + +PydanticModels = Union[ + StandardTypesModel, + StrictStandardTypesModel, + ComplexTypesModel, + SpecialTypesModel, + StrictSpecialTypesModel, + ParentModel, + FieldFeaturesModel, + AnnotatedFieldsModel, + GenericModel[Any], + UnionModel, + PydanticDatetimeModel, + PydanticDateModel, + PydanticTimedeltaModel, +] + + +def make_list_of_pydantic_objects() -> List[PydanticModels]: + objects = [ + make_standard_types_object(), + make_strict_standard_types_object(), + make_complex_types_object(), + make_special_types_object(), + make_strict_special_types_object(), + make_nested_object(), + make_field_features_object(), + make_annotated_fields_object(), + make_generic_string_object(), + make_union_object(), + make_pydantic_datetime_object(), + make_pydantic_date_object(), + make_pydantic_timedelta_object(), + ] + for o in objects: + o._check_instance() # type: ignore + return objects # type: ignore + + +@dataclasses.dataclass(order=True) +class MyDataClass: + # The name int_field also occurs in StandardTypesModel and currently unions can match them up incorrectly. + data_class_int_field: int + + +def make_dataclass_objects() -> List[MyDataClass]: + return [MyDataClass(data_class_int_field=7)] + + +ComplexCustomType = Tuple[List[MyDataClass], List[PydanticModels]] +ComplexCustomUnionType = List[Union[MyDataClass, PydanticModels]] + + +class PydanticModelWithStrictField(BaseModel): + strict_field: datetime = Field(strict=True) diff --git a/tests/contrib/pydantic/models_2.py b/tests/contrib/pydantic/models_2.py new file mode 100644 index 00000000..5e9f51b2 --- /dev/null +++ b/tests/contrib/pydantic/models_2.py @@ -0,0 +1,250 @@ +import collections +import decimal +import fractions +import re +from enum import Enum, IntEnum +from typing import ( + Any, + Dict, + Hashable, + List, + NamedTuple, + Optional, + Pattern, + Sequence, + Set, + Tuple, + Union, + cast, +) + +from pydantic import BaseModel +from typing_extensions import TypedDict + + +class FruitEnum(str, Enum): + apple = "apple" + banana = "banana" + + +class NumberEnum(IntEnum): + one = 1 + two = 2 + + +class UserTypedDict(TypedDict): + name: str + id: int + + +class TypedDictModel(BaseModel): + typed_dict_field: UserTypedDict + + def _check_instance(self) -> None: + assert isinstance(self.typed_dict_field, dict) + assert self.typed_dict_field == {"name": "username", "id": 7} + + +def make_typed_dict_object() -> TypedDictModel: + return TypedDictModel(typed_dict_field={"name": "username", "id": 7}) + + +class StandardTypesModel(BaseModel): + # Boolean + bool_field: bool + bool_field_int: bool + bool_field_str: bool + + # Numbers + int_field: int + float_field: float + decimal_field: decimal.Decimal + complex_field: complex + fraction_field: fractions.Fraction + + # Strings and Bytes + str_field: str + bytes_field: bytes + + # None + none_field: None + + # Enums + str_enum_field: FruitEnum + int_enum_field: NumberEnum + + # Collections + list_field: list + tuple_field: tuple + set_field: set + frozenset_field: frozenset + deque_field: collections.deque + sequence_field: Sequence[int] + # Iterable[int] supported but not tested since original vs round-tripped do not compare equal + + # Mappings + dict_field: dict + defaultdict_field: collections.defaultdict[str, int] + counter_field: collections.Counter + typed_dict_field: UserTypedDict + + # Other Types + pattern_field: Pattern + hashable_field: Hashable + any_field: Any + + def _check_instance(self) -> None: + # Boolean checks + assert isinstance(self.bool_field, bool) + assert self.bool_field is True + assert isinstance(self.bool_field_int, bool) + assert self.bool_field_int is True + assert isinstance(self.bool_field_str, bool) + assert self.bool_field_str is True + + # Number checks + assert isinstance(self.int_field, int) + assert self.int_field == 42 + assert isinstance(self.float_field, float) + assert self.float_field == 3.14 + assert isinstance(self.decimal_field, decimal.Decimal) + assert self.decimal_field == decimal.Decimal("3.14") + assert isinstance(self.complex_field, complex) + assert self.complex_field == complex(1, 2) + assert isinstance(self.fraction_field, fractions.Fraction) + assert self.fraction_field == fractions.Fraction(22, 7) + + # String and Bytes checks + assert isinstance(self.str_field, str) + assert self.str_field == "hello" + assert isinstance(self.bytes_field, bytes) + assert self.bytes_field == b"world" + + # None check + assert self.none_field is None + + # Enum checks + assert isinstance(self.str_enum_field, Enum) + assert isinstance(self.int_enum_field, IntEnum) + + # Collection checks + assert isinstance(self.list_field, list) + assert self.list_field == [1, 2, 3] + assert isinstance(self.tuple_field, tuple) + assert self.tuple_field == (1, 2, 3) + assert isinstance(self.set_field, set) + assert self.set_field == {1, 2, 3} + assert isinstance(self.frozenset_field, frozenset) + assert self.frozenset_field == frozenset([1, 2, 3]) + assert isinstance(self.deque_field, collections.deque) + assert list(self.deque_field) == [1, 2, 3] + assert isinstance(self.sequence_field, list) + assert list(self.sequence_field) == [1, 2, 3] + + # Mapping checks + assert isinstance(self.dict_field, dict) + assert self.dict_field == {"a": 1, "b": 2} + assert isinstance(self.defaultdict_field, collections.defaultdict) + assert dict(self.defaultdict_field) == {"a": 1, "b": 2} + assert isinstance(self.counter_field, collections.Counter) + assert dict(self.counter_field) == {"a": 1, "b": 2} + assert isinstance(self.typed_dict_field, dict) + assert self.typed_dict_field == {"name": "username", "id": 7} + + # Other type checks + assert isinstance(self.pattern_field, Pattern) + assert self.pattern_field.pattern == r"\d+" + assert isinstance(self.hashable_field, Hashable) + assert self.hashable_field == "test" + assert self.any_field == "anything goes" + + +def make_standard_types_object() -> StandardTypesModel: + return StandardTypesModel( + # Boolean + bool_field=True, + bool_field_int=1, # type: ignore + bool_field_str="true", # type: ignore + # Numbers + int_field=42, + float_field=3.14, + decimal_field=decimal.Decimal("3.14"), + complex_field=complex(1, 2), + fraction_field=fractions.Fraction(22, 7), + # Strings and Bytes + str_field="hello", + bytes_field=b"world", + # None + none_field=None, + # Enums + str_enum_field=FruitEnum.apple, + int_enum_field=NumberEnum.one, + # Collections + # these cast input to list, tuple, set, etc. + list_field={1, 2, 3}, # type: ignore + tuple_field=(1, 2, 3), + set_field={1, 2, 3}, + frozenset_field=frozenset([1, 2, 3]), + deque_field=collections.deque([1, 2, 3]), + # other sequence types are converted to list, as documented + sequence_field=[1, 2, 3], + # Mappings + dict_field={"a": 1, "b": 2}, + defaultdict_field=collections.defaultdict(int, {"a": 1, "b": 2}), + counter_field=collections.Counter({"a": 1, "b": 2}), + typed_dict_field={"name": "username", "id": 7}, + # Other Types + pattern_field=re.compile(r"\d+"), + hashable_field="test", + any_field="anything goes", + ) + + +class StrictStandardTypesModel(StandardTypesModel, strict=True): + pass + + +def make_strict_standard_types_object() -> StrictStandardTypesModel: + return cast(StrictStandardTypesModel, make_standard_types_object()) + + +class Point(NamedTuple): + x: int + y: int + + +class ComplexTypesModel(BaseModel): + list_field: List[str] + dict_field: Dict[str, int] + set_field: Set[int] + tuple_field: Tuple[str, int] + union_field: Union[str, int] + optional_field: Optional[str] + named_tuple_field: Point + + def _check_instance(self) -> None: + assert isinstance(self.list_field, list) + assert isinstance(self.dict_field, dict) + assert isinstance(self.set_field, set) + assert isinstance(self.tuple_field, tuple) + assert isinstance(self.union_field, str) + assert isinstance(self.optional_field, str) + assert self.list_field == ["a", "b", "c"] + assert self.dict_field == {"x": 1, "y": 2} + assert self.set_field == {1, 2, 3} + assert self.tuple_field == ("hello", 42) + assert self.union_field == "string_or_int" + assert self.optional_field == "present" + assert self.named_tuple_field == Point(x=1, y=2) + + +def make_complex_types_object() -> ComplexTypesModel: + return ComplexTypesModel( + list_field=["a", "b", "c"], + dict_field={"x": 1, "y": 2}, + set_field={1, 2, 3}, + tuple_field=("hello", 42), + union_field="string_or_int", + optional_field="present", + named_tuple_field=Point(x=1, y=2), + ) diff --git a/tests/contrib/pydantic/test_pydantic.py b/tests/contrib/pydantic/test_pydantic.py new file mode 100644 index 00000000..26764b40 --- /dev/null +++ b/tests/contrib/pydantic/test_pydantic.py @@ -0,0 +1,326 @@ +import dataclasses +import uuid +from datetime import datetime + +import pydantic +import pytest +from pydantic import BaseModel + +from temporalio.client import Client +from temporalio.contrib.pydantic import pydantic_data_converter +from temporalio.worker import Worker +from tests.contrib.pydantic.models import ( + PydanticModels, + PydanticModelWithStrictField, + make_dataclass_objects, + make_list_of_pydantic_objects, +) +from tests.contrib.pydantic.workflows import ( + CloneObjectsWorkflow, + ComplexCustomTypeWorkflow, + ComplexCustomUnionTypeWorkflow, + DatetimeUsageWorkflow, + InstantiateModelsWorkflow, + NoTypeAnnotationsWorkflow, + PydanticModelUsageWorkflow, + PydanticModelWithStrictFieldWorkflow, + RoundTripMiscObjectsWorkflow, + RoundTripPydanticObjectsWorkflow, + _test_pydantic_model_with_strict_field, + clone_objects, + misc_objects_activity, + pydantic_objects_activity, +) + + +async def test_instantiation_outside_sandbox(): + make_list_of_pydantic_objects() + + +async def test_instantiation_inside_sandbox(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[InstantiateModelsWorkflow], + ): + await client.execute_workflow( + InstantiateModelsWorkflow.run, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + + +@pytest.mark.parametrize("typed", [True, False]) +async def test_round_trip_pydantic_objects(client: Client, typed: bool): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + orig_objects = make_list_of_pydantic_objects() + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[RoundTripPydanticObjectsWorkflow], + activities=[pydantic_objects_activity], + ): + if typed: + returned_objects = await client.execute_workflow( + RoundTripPydanticObjectsWorkflow.run, + orig_objects, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + else: + returned_objects = await client.execute_workflow( + "RoundTripPydanticObjectsWorkflow", + orig_objects, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + result_type=list[PydanticModels], + ) + + assert returned_objects == orig_objects + for o in returned_objects: + o._check_instance() + + +async def test_round_trip_misc_objects(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + orig_objects = ( + 7, + "7", + {"7": 7.0}, + [{"7": 7.0}], + ({"7": 7.0},), + datetime(2025, 1, 2, 3, 4, 5), + uuid.uuid4(), + ) + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[RoundTripMiscObjectsWorkflow], + activities=[misc_objects_activity], + ): + returned_objects = await client.execute_workflow( + RoundTripMiscObjectsWorkflow.run, + orig_objects, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + assert returned_objects == orig_objects + + +async def test_clone_objects_outside_sandbox(): + clone_objects(make_list_of_pydantic_objects()) + + +async def test_clone_objects_in_sandbox(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + orig_objects = make_list_of_pydantic_objects() + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[CloneObjectsWorkflow], + ): + returned_objects = await client.execute_workflow( + CloneObjectsWorkflow.run, + orig_objects, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + assert returned_objects == orig_objects + for o in returned_objects: + o._check_instance() + + +async def test_complex_custom_type(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + orig_dataclass_objects = make_dataclass_objects() + orig_pydantic_objects = make_list_of_pydantic_objects() + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[ComplexCustomTypeWorkflow], + activities=[pydantic_objects_activity], + ): + ( + returned_dataclass_objects, + returned_pydantic_objects, + ) = await client.execute_workflow( + ComplexCustomTypeWorkflow.run, + (orig_dataclass_objects, orig_pydantic_objects), + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + assert orig_dataclass_objects == returned_dataclass_objects + assert orig_pydantic_objects == returned_pydantic_objects + for o in returned_pydantic_objects: + o._check_instance() + + +async def test_complex_custom_union_type(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + orig_dataclass_objects = make_dataclass_objects() + orig_pydantic_objects = make_list_of_pydantic_objects() + orig_objects = orig_dataclass_objects + orig_pydantic_objects + import random + + random.shuffle(orig_objects) + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[ComplexCustomUnionTypeWorkflow], + activities=[pydantic_objects_activity], + ): + returned_objects = await client.execute_workflow( + ComplexCustomUnionTypeWorkflow.run, + orig_objects, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + returned_dataclass_objects = [] + returned_pydantic_objects: list[BaseModel] = [] + for o in returned_objects: + if dataclasses.is_dataclass(o): + returned_dataclass_objects.append(o) + elif isinstance(o, BaseModel): + returned_pydantic_objects.append(o) + else: + raise TypeError(f"Unexpected type: {type(o)}") + assert sorted(orig_dataclass_objects, key=lambda o: o.__class__.__name__) == sorted( + returned_dataclass_objects, key=lambda o: o.__class__.__name__ + ) + assert sorted(orig_pydantic_objects, key=lambda o: o.__class__.__name__) == sorted( + returned_pydantic_objects, key=lambda o: o.__class__.__name__ + ) + for o2 in returned_pydantic_objects: + o2._check_instance() # type: ignore + + +async def test_pydantic_model_usage_in_workflow(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[PydanticModelUsageWorkflow], + ): + await client.execute_workflow( + PydanticModelUsageWorkflow.run, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + + +async def test_datetime_usage_in_workflow(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[DatetimeUsageWorkflow], + ): + await client.execute_workflow( + DatetimeUsageWorkflow.run, + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + + +def test_pydantic_model_with_strict_field_outside_sandbox(): + _test_pydantic_model_with_strict_field( + PydanticModelWithStrictField(strict_field=datetime(2025, 1, 2, 3, 4, 5)) + ) + + +async def test_pydantic_model_with_strict_field_inside_sandbox(client: Client): + client_config = client.config() + client_config["data_converter"] = pydantic_data_converter + client = Client(**client_config) + tq = str(uuid.uuid4()) + async with Worker( + client, + workflows=[PydanticModelWithStrictFieldWorkflow], + task_queue=tq, + ): + orig = PydanticModelWithStrictField(strict_field=datetime(2025, 1, 2, 3, 4, 5)) + result = await client.execute_workflow( + PydanticModelWithStrictFieldWorkflow.run, + orig, + id=str(uuid.uuid4()), + task_queue=tq, + ) + assert result == orig + + +async def test_no_type_annotations(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + async with Worker( + client, + task_queue=task_queue_name, + workflows=[NoTypeAnnotationsWorkflow], + ): + result = await client.execute_workflow( + "NoTypeAnnotationsWorkflow", + (7,), + id=str(uuid.uuid4()), + task_queue=task_queue_name, + ) + assert result == [7] + + +async def test_validation_error(client: Client): + new_config = client.config() + new_config["data_converter"] = pydantic_data_converter + client = Client(**new_config) + task_queue_name = str(uuid.uuid4()) + + async with Worker( + client, + task_queue=task_queue_name, + workflows=[NoTypeAnnotationsWorkflow], + ): + with pytest.raises(pydantic.ValidationError): + await client.execute_workflow( + "NoTypeAnnotationsWorkflow", + "not-an-int", + id=str(uuid.uuid4()), + task_queue=task_queue_name, + result_type=tuple[int], + ) diff --git a/tests/contrib/pydantic/workflows.py b/tests/contrib/pydantic/workflows.py new file mode 100644 index 00000000..a4d656b2 --- /dev/null +++ b/tests/contrib/pydantic/workflows.py @@ -0,0 +1,175 @@ +import dataclasses +from datetime import datetime, timedelta +from typing import List +from uuid import UUID + +from pydantic import BaseModel, create_model + +from temporalio import workflow + +with workflow.unsafe.imports_passed_through(): + from tests.contrib.pydantic.activities import ( + misc_objects_activity, + pydantic_objects_activity, + ) + +from tests.contrib.pydantic.models import ( + ComplexCustomType, + ComplexCustomUnionType, + PydanticModels, + PydanticModelWithStrictField, + make_list_of_pydantic_objects, +) + + +def clone_objects(objects: List[PydanticModels]) -> List[PydanticModels]: + new_objects = [] + for o in objects: + fields = {} + for name, f in o.model_fields.items(): + fields[name] = (f.annotation, f) + model = create_model(o.__class__.__name__, **fields) # type: ignore + new_objects.append(model(**o.model_dump(by_alias=True))) + for old, new in zip(objects, new_objects): + assert old.model_dump() == new.model_dump() + return new_objects + + +@workflow.defn +class InstantiateModelsWorkflow: + @workflow.run + async def run(self) -> None: + make_list_of_pydantic_objects() + + +@workflow.defn +class RoundTripPydanticObjectsWorkflow: + @workflow.run + async def run(self, objects: List[PydanticModels]) -> List[PydanticModels]: + return await workflow.execute_activity( + pydantic_objects_activity, + objects, + start_to_close_timeout=timedelta(minutes=1), + ) + + +@workflow.defn +class RoundTripMiscObjectsWorkflow: + @workflow.run + async def run( + self, + objects: tuple[ + int, + str, + dict[str, float], + list[dict[str, float]], + tuple[dict[str, float]], + datetime, + UUID, + ], + ) -> tuple[ + int, + str, + dict[str, float], + list[dict[str, float]], + tuple[dict[str, float]], + datetime, + UUID, + ]: + return await workflow.execute_activity( + misc_objects_activity, + objects, + start_to_close_timeout=timedelta(minutes=1), + ) + + +@workflow.defn +class CloneObjectsWorkflow: + @workflow.run + async def run(self, objects: List[PydanticModels]) -> List[PydanticModels]: + return clone_objects(objects) + + +@workflow.defn +class ComplexCustomUnionTypeWorkflow: + @workflow.run + async def run( + self, + input: ComplexCustomUnionType, + ) -> ComplexCustomUnionType: + data_classes = [] + pydantic_objects: List[PydanticModels] = [] + for o in input: + if dataclasses.is_dataclass(o): + data_classes.append(o) + elif isinstance(o, BaseModel): + pydantic_objects.append(o) + else: + raise TypeError(f"Unexpected type: {type(o)}") + pydantic_objects = await workflow.execute_activity( + pydantic_objects_activity, + pydantic_objects, + start_to_close_timeout=timedelta(minutes=1), + ) + return data_classes + pydantic_objects # type: ignore + + +@workflow.defn +class ComplexCustomTypeWorkflow: + @workflow.run + async def run( + self, + input: ComplexCustomType, + ) -> ComplexCustomType: + data_classes, pydantic_objects = input + pydantic_objects = await workflow.execute_activity( + pydantic_objects_activity, + pydantic_objects, + start_to_close_timeout=timedelta(minutes=1), + ) + return data_classes, pydantic_objects + + +@workflow.defn +class PydanticModelUsageWorkflow: + @workflow.run + async def run(self) -> None: + for o in make_list_of_pydantic_objects(): + o._check_instance() + + +@workflow.defn +class DatetimeUsageWorkflow: + @workflow.run + async def run(self) -> None: + dt = workflow.now() + assert isinstance(dt, datetime) + assert issubclass(dt.__class__, datetime) + + +def _test_pydantic_model_with_strict_field( + obj: PydanticModelWithStrictField, +): + roundtripped = PydanticModelWithStrictField.model_validate(obj.model_dump()) + assert roundtripped == obj + roundtripped2 = PydanticModelWithStrictField.model_validate_json( + obj.model_dump_json() + ) + assert roundtripped2 == obj + return roundtripped + + +@workflow.defn +class PydanticModelWithStrictFieldWorkflow: + @workflow.run + async def run( + self, obj: PydanticModelWithStrictField + ) -> PydanticModelWithStrictField: + return _test_pydantic_model_with_strict_field(obj) + + +@workflow.defn +class NoTypeAnnotationsWorkflow: + @workflow.run + async def run(self, arg): + return arg diff --git a/tests/worker/workflow_sandbox/test_restrictions.py b/tests/worker/workflow_sandbox/test_restrictions.py index 90d25454..cf96d28d 100644 --- a/tests/worker/workflow_sandbox/test_restrictions.py +++ b/tests/worker/workflow_sandbox/test_restrictions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import pathlib import sys from dataclasses import dataclass from typing import ClassVar, Dict, Optional @@ -28,7 +29,7 @@ def test_workflow_sandbox_stdlib_module_names(): if len(code_lines[-1]) > 80: code_lines.append("") code_lines[-1] += mod_name - code = f'_stdlib_module_names = (\n "' + '"\n "'.join(code_lines) + '"\n)' + code = '_stdlib_module_names = (\n "' + '"\n "'.join(code_lines) + '"\n)' # TODO(cretz): Point releases may add modules :-( assert ( actual_names == _stdlib_module_names @@ -56,6 +57,45 @@ class RestrictableObject: RestrictableObject.qux = RestrictableObject(foo=RestrictableObject(bar=70), bar=80) +class RestrictableClass: + def __str__(self): + return "__str__" + + def __repr__(self): + return "__repr__" + + def __format__(self, __format_spec: str) -> str: + return "__format__" + + +def test_restricted_proxy_dunder_methods(): + restricted_class = _RestrictedProxy( + "RestrictableClass", + RestrictableClass, + RestrictionContext(), + SandboxMatcher(), + ) + restricted_obj = restricted_class() + assert type(restricted_obj) is _RestrictedProxy + assert str(restricted_obj) == "__str__" + assert repr(restricted_obj) == "__repr__" + assert format(restricted_obj, "") == "__format__" + assert f"{restricted_obj}" == "__format__" + + restricted_path = _RestrictedProxy( + "Path", + pathlib.Path, + RestrictionContext(), + SandboxMatcher(), + ) + assert isinstance(format(restricted_path, ""), str) + restricted_path_obj = restricted_path("test/path") + assert type(restricted_path_obj) is _RestrictedProxy + expected_path = str(pathlib.PurePath("test/path")) + assert format(restricted_path_obj, "") == expected_path + assert f"{restricted_path_obj}" == expected_path + + def test_workflow_sandbox_restricted_proxy(): obj_class = _RestrictedProxy( "RestrictableObject", diff --git a/tests/worker/workflow_sandbox/test_runner.py b/tests/worker/workflow_sandbox/test_runner.py index a83c5eff..14b2c94c 100644 --- a/tests/worker/workflow_sandbox/test_runner.py +++ b/tests/worker/workflow_sandbox/test_runner.py @@ -12,10 +12,8 @@ from enum import IntEnum from typing import Callable, Dict, List, Optional, Sequence, Set, Type -import pydantic import pytest -import temporalio.worker.workflow_sandbox._restrictions from temporalio import activity, workflow from temporalio.client import Client, WorkflowFailureError, WorkflowHandle from temporalio.exceptions import ApplicationError @@ -263,10 +261,6 @@ async def test_workflow_sandbox_restrictions(client: Client): class DateOperatorWorkflow: @workflow.run async def run(self) -> int: - assert ( - type(date(2010, 1, 20)) - == temporalio.worker.workflow_sandbox._restrictions._RestrictedProxy - ) return (date(2010, 1, 20) - date(2010, 1, 1)).days @@ -390,10 +384,6 @@ async def test_workflow_sandbox_with_proto(client: Client): assert result is not param and result == param -class PydanticMessage(pydantic.BaseModel): - content: datetime - - @workflow.defn class KnownIssuesWorkflow: @workflow.run @@ -413,14 +403,6 @@ async def run(self) -> None: except RuntimeError as err: assert "Restriction state not present" in str(err) - # Using a datetime in binary-compiled Pydantic skips our issubclass when - # building their validators causing it to use date instead - # TODO(cretz): https://github.com/temporalio/sdk-python/issues/207 - if pydantic.compiled: - assert isinstance(PydanticMessage(content=workflow.now()).content, date) - else: - assert isinstance(PydanticMessage(content=workflow.now()).content, datetime) - async def test_workflow_sandbox_known_issues(client: Client): async with new_worker(client, KnownIssuesWorkflow) as worker: