Compare changes

Choose any two refs to compare.

+41
src/atpasser/model/typed.py
···
··· 1 + from typing import Any 2 + from pydantic import field_serializer 3 + from .base import DataModel 4 + 5 + class TypedDataModel(DataModel): 6 + """ 7 + Model for AT Protocol data with type information. 8 + 9 + Includes support for $type field that specifies Lexicon schema. 10 + """ 11 + 12 + type: str | None = None 13 + """Lexicon schema type identifier""" 14 + 15 + def __init__(self, **data: Any) -> None: 16 + """ 17 + Initialize typed data model with automatic $type handling. 18 + 19 + Args: 20 + **data: Data including optional $type field 21 + """ 22 + # Extract $type if present 23 + dataType = data.pop("$type", None) 24 + if dataType: 25 + data["type"] = dataType 26 + super().__init__(**data) 27 + 28 + @field_serializer("type") 29 + def serializeType(self, v: str | None) -> dict[str, str] | None: 30 + """ 31 + Serialize type field to $type object. 32 + 33 + Args: 34 + v: Type value to serialize 35 + 36 + Returns: 37 + $type object if type is not None 38 + """ 39 + if v is not None: 40 + return {"$type": v} 41 + return None
+36
src/atpasser/model/exceptions.py
···
··· 1 + class AtprotoModelError(Exception): 2 + """Base exception for all AT Protocol model errors""" 3 + pass 4 + 5 + class ValidationError(AtprotoModelError): 6 + """Raised when data validation fails""" 7 + def __init__(self, field: str, message: str): 8 + self.field = field 9 + self.message = message 10 + super().__init__(f"Validation error for field '{field}': {message}") 11 + 12 + class SerializationError(AtprotoModelError): 13 + """Raised when data serialization fails""" 14 + def __init__(self, field: str, message: str): 15 + self.field = field 16 + self.message = message 17 + super().__init__(f"Serialization error for field '{field}': {message}") 18 + 19 + class DeserializationError(AtprotoModelError): 20 + """Raised when data deserialization fails""" 21 + def __init__(self, field: str, message: str): 22 + self.field = field 23 + self.message = message 24 + super().__init__(f"Deserialization error for field '{field}': {message}") 25 + 26 + class InvalidCIDError(AtprotoModelError): 27 + """Raised when CID validation fails""" 28 + pass 29 + 30 + class InvalidBlobError(AtprotoModelError): 31 + """Raised when blob validation fails""" 32 + pass 33 + 34 + class TypeMismatchError(AtprotoModelError): 35 + """Raised when type validation fails""" 36 + pass
+3
src/atpasser/model/__init__.py
··· 38 ProcedureModel, 39 SubscriptionModel 40 ) 41 42 __all__ = [ 43 "DataModel", ··· 61 "QueryModel", 62 "ProcedureModel", 63 "SubscriptionModel", 64 # Exceptions 65 "AtprotoModelError", 66 "ValidationError",
··· 38 ProcedureModel, 39 SubscriptionModel 40 ) 41 + from .converter import LexiconConverter 42 43 __all__ = [ 44 "DataModel", ··· 62 "QueryModel", 63 "ProcedureModel", 64 "SubscriptionModel", 65 + # Converter 66 + "LexiconConverter", 67 # Exceptions 68 "AtprotoModelError", 69 "ValidationError",
-5
src/atpasser/model/base.py
··· 1 import base64 2 - import re 3 - from datetime import datetime 4 from typing import Any 5 - from collections.abc import Mapping 6 from cid.cid import CIDv1, make_cid 7 from pydantic import BaseModel, field_serializer, field_validator, ConfigDict 8 - from pydantic.fields import FieldInfo 9 from .exceptions import ( 10 - ValidationError, 11 SerializationError, 12 DeserializationError, 13 InvalidCIDError
··· 1 import base64 2 from typing import Any 3 from cid.cid import CIDv1, make_cid 4 from pydantic import BaseModel, field_serializer, field_validator, ConfigDict 5 from .exceptions import ( 6 SerializationError, 7 DeserializationError, 8 InvalidCIDError
+5 -5
src/atpasser/model/blob.py
··· 1 from typing import Any 2 from pydantic import field_validator, ConfigDict 3 from .base import DataModel 4 - from .exceptions import ValidationError, InvalidBlobError 5 6 class BlobModel(DataModel): 7 """ ··· 34 Validated size 35 36 Raises: 37 - ValueError: If size is not positive 38 """ 39 if v <= 0: 40 - raise ValueError("Blob size must be positive and non-zero") 41 return v 42 43 @field_validator("mimeType") ··· 52 Validated MIME type 53 54 Raises: 55 - ValueError: If MIME type is empty 56 """ 57 if not v: 58 - raise ValueError("MIME type cannot be empty") 59 return v
··· 1 from typing import Any 2 from pydantic import field_validator, ConfigDict 3 from .base import DataModel 4 + from .exceptions import ValidationError 5 6 class BlobModel(DataModel): 7 """ ··· 34 Validated size 35 36 Raises: 37 + ValidationError: If size is not positive 38 """ 39 if v <= 0: 40 + raise ValidationError(field="size", message="must be positive and non-zero") 41 return v 42 43 @field_validator("mimeType") ··· 52 Validated MIME type 53 54 Raises: 55 + ValidationError: If MIME type is empty 56 """ 57 if not v: 58 + raise ValidationError(field="mimeType", message="cannot be empty") 59 return v
+323 -10
poetry.lock
··· 103 description = "Python package for providing Mozilla's CA Bundle." 104 optional = false 105 python-versions = ">=3.7" 106 - groups = ["main"] 107 files = [ 108 {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, 109 {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ··· 115 description = "Foreign Function Interface for Python calling C code." 116 optional = false 117 python-versions = ">=3.9" 118 - groups = ["main"] 119 markers = "platform_python_implementation != \"PyPy\"" 120 files = [ 121 {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, ··· 213 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 214 optional = false 215 python-versions = ">=3.7" 216 - groups = ["main"] 217 files = [ 218 {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, 219 {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, ··· 296 {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, 297 ] 298 299 [[package]] 300 name = "cryptography" 301 version = "45.0.7" 302 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 303 optional = false 304 python-versions = "!=3.9.0,!=3.9.1,>=3.7" 305 - groups = ["main"] 306 files = [ 307 {file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"}, 308 {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"}, ··· 432 description = "Internationalized Domain Names in Applications (IDNA)" 433 optional = false 434 python-versions = ">=3.6" 435 - groups = ["main"] 436 files = [ 437 {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 438 {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ··· 441 [package.extras] 442 all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 443 444 [[package]] 445 name = "jsonpath-ng" 446 version = "1.7.0" ··· 740 {file = "morphys-1.0-py2.py3-none-any.whl", hash = "sha256:76d6dbaa4d65f597e59d332c81da786d83e4669387b9b2a750cfec74e7beec20"}, 741 ] 742 743 [[package]] 744 name = "ply" 745 version = "3.11" ··· 815 description = "C parser in Python" 816 optional = false 817 python-versions = ">=3.8" 818 - groups = ["main"] 819 markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" 820 files = [ 821 {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, ··· 956 [package.dependencies] 957 typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 958 959 [[package]] 960 name = "pyld" 961 version = "2.0.4" ··· 996 blake2 = ["pyblake2"] 997 sha3 = ["pysha3"] 998 999 [[package]] 1000 name = "python-baseconv" 1001 version = "1.2.2" ··· 1013 description = "Python HTTP for Humans." 1014 optional = false 1015 python-versions = ">=3.9" 1016 - groups = ["main"] 1017 files = [ 1018 {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, 1019 {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ··· 1047 description = "Backported and Experimental Type Hints for Python 3.9+" 1048 optional = false 1049 python-versions = ">=3.9" 1050 - groups = ["main"] 1051 files = [ 1052 {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 1053 {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ··· 1074 description = "HTTP library with thread-safe connection pooling, file post, and more." 1075 optional = false 1076 python-versions = ">=3.9" 1077 - groups = ["main"] 1078 files = [ 1079 {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, 1080 {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ··· 1100 [metadata] 1101 lock-version = "2.1" 1102 python-versions = ">=3.13" 1103 - content-hash = "4919ab150fee9e4e358e57bada62225cb2d92c52509b26169db269691b86cefe"
··· 103 description = "Python package for providing Mozilla's CA Bundle." 104 optional = false 105 python-versions = ">=3.7" 106 + groups = ["main", "dev"] 107 files = [ 108 {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, 109 {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ··· 115 description = "Foreign Function Interface for Python calling C code." 116 optional = false 117 python-versions = ">=3.9" 118 + groups = ["main", "dev"] 119 markers = "platform_python_implementation != \"PyPy\"" 120 files = [ 121 {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, ··· 213 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 214 optional = false 215 python-versions = ">=3.7" 216 + groups = ["main", "dev"] 217 files = [ 218 {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, 219 {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, ··· 296 {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, 297 ] 298 299 + [[package]] 300 + name = "colorama" 301 + version = "0.4.6" 302 + description = "Cross-platform colored terminal text." 303 + optional = false 304 + python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 305 + groups = ["dev"] 306 + markers = "sys_platform == \"win32\"" 307 + files = [ 308 + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 309 + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 310 + ] 311 + 312 + [[package]] 313 + name = "coverage" 314 + version = "7.10.7" 315 + description = "Code coverage measurement for Python" 316 + optional = false 317 + python-versions = ">=3.9" 318 + groups = ["dev"] 319 + files = [ 320 + {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, 321 + {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, 322 + {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, 323 + {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, 324 + {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, 325 + {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, 326 + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, 327 + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, 328 + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, 329 + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, 330 + {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, 331 + {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, 332 + {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, 333 + {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, 334 + {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, 335 + {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, 336 + {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, 337 + {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, 338 + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, 339 + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, 340 + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, 341 + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, 342 + {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, 343 + {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, 344 + {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, 345 + {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, 346 + {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, 347 + {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, 348 + {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, 349 + {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, 350 + {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, 351 + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, 352 + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, 353 + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, 354 + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, 355 + {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, 356 + {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, 357 + {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, 358 + {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, 359 + {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, 360 + {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, 361 + {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, 362 + {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, 363 + {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, 364 + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, 365 + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, 366 + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, 367 + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, 368 + {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, 369 + {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, 370 + {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, 371 + {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, 372 + {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, 373 + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, 374 + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, 375 + {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, 376 + {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, 377 + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, 378 + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, 379 + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, 380 + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, 381 + {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, 382 + {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, 383 + {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, 384 + {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, 385 + {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, 386 + {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, 387 + {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, 388 + {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, 389 + {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, 390 + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, 391 + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, 392 + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, 393 + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, 394 + {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, 395 + {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, 396 + {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, 397 + {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, 398 + {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, 399 + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, 400 + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, 401 + {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, 402 + {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, 403 + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, 404 + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, 405 + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, 406 + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, 407 + {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, 408 + {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, 409 + {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, 410 + {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, 411 + {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, 412 + {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, 413 + {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, 414 + {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, 415 + {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, 416 + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, 417 + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, 418 + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, 419 + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, 420 + {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, 421 + {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, 422 + {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, 423 + {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, 424 + ] 425 + 426 + [package.extras] 427 + toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 428 + 429 [[package]] 430 name = "cryptography" 431 version = "45.0.7" 432 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 433 optional = false 434 python-versions = "!=3.9.0,!=3.9.1,>=3.7" 435 + groups = ["main", "dev"] 436 files = [ 437 {file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"}, 438 {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"}, ··· 562 description = "Internationalized Domain Names in Applications (IDNA)" 563 optional = false 564 python-versions = ">=3.6" 565 + groups = ["main", "dev"] 566 files = [ 567 {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 568 {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ··· 571 [package.extras] 572 all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 573 574 + [[package]] 575 + name = "iniconfig" 576 + version = "2.1.0" 577 + description = "brain-dead simple config-ini parsing" 578 + optional = false 579 + python-versions = ">=3.8" 580 + groups = ["dev"] 581 + files = [ 582 + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 583 + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 584 + ] 585 + 586 [[package]] 587 name = "jsonpath-ng" 588 version = "1.7.0" ··· 882 {file = "morphys-1.0-py2.py3-none-any.whl", hash = "sha256:76d6dbaa4d65f597e59d332c81da786d83e4669387b9b2a750cfec74e7beec20"}, 883 ] 884 885 + [[package]] 886 + name = "packaging" 887 + version = "25.0" 888 + description = "Core utilities for Python packages" 889 + optional = false 890 + python-versions = ">=3.8" 891 + groups = ["dev"] 892 + files = [ 893 + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 894 + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 895 + ] 896 + 897 + [[package]] 898 + name = "pluggy" 899 + version = "1.6.0" 900 + description = "plugin and hook calling mechanisms for python" 901 + optional = false 902 + python-versions = ">=3.9" 903 + groups = ["dev"] 904 + files = [ 905 + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 906 + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 907 + ] 908 + 909 + [package.extras] 910 + dev = ["pre-commit", "tox"] 911 + testing = ["coverage", "pytest", "pytest-benchmark"] 912 + 913 [[package]] 914 name = "ply" 915 version = "3.11" ··· 985 description = "C parser in Python" 986 optional = false 987 python-versions = ">=3.8" 988 + groups = ["main", "dev"] 989 markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" 990 files = [ 991 {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, ··· 1126 [package.dependencies] 1127 typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 1128 1129 + [[package]] 1130 + name = "pygithub" 1131 + version = "2.8.1" 1132 + description = "Use the full Github API v3" 1133 + optional = false 1134 + python-versions = ">=3.8" 1135 + groups = ["dev"] 1136 + files = [ 1137 + {file = "pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0"}, 1138 + {file = "pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9"}, 1139 + ] 1140 + 1141 + [package.dependencies] 1142 + pyjwt = {version = ">=2.4.0", extras = ["crypto"]} 1143 + pynacl = ">=1.4.0" 1144 + requests = ">=2.14.0" 1145 + typing-extensions = ">=4.5.0" 1146 + urllib3 = ">=1.26.0" 1147 + 1148 + [[package]] 1149 + name = "pygments" 1150 + version = "2.19.2" 1151 + description = "Pygments is a syntax highlighting package written in Python." 1152 + optional = false 1153 + python-versions = ">=3.8" 1154 + groups = ["dev"] 1155 + files = [ 1156 + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 1157 + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 1158 + ] 1159 + 1160 + [package.extras] 1161 + windows-terminal = ["colorama (>=0.4.6)"] 1162 + 1163 + [[package]] 1164 + name = "pyjwt" 1165 + version = "2.10.1" 1166 + description = "JSON Web Token implementation in Python" 1167 + optional = false 1168 + python-versions = ">=3.9" 1169 + groups = ["dev"] 1170 + files = [ 1171 + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, 1172 + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, 1173 + ] 1174 + 1175 + [package.dependencies] 1176 + cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} 1177 + 1178 + [package.extras] 1179 + crypto = ["cryptography (>=3.4.0)"] 1180 + dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] 1181 + docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 1182 + tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 1183 + 1184 [[package]] 1185 name = "pyld" 1186 version = "2.0.4" ··· 1221 blake2 = ["pyblake2"] 1222 sha3 = ["pysha3"] 1223 1224 + [[package]] 1225 + name = "pynacl" 1226 + version = "1.6.0" 1227 + description = "Python binding to the Networking and Cryptography (NaCl) library" 1228 + optional = false 1229 + python-versions = ">=3.8" 1230 + groups = ["dev"] 1231 + files = [ 1232 + {file = "pynacl-1.6.0-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb"}, 1233 + {file = "pynacl-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348"}, 1234 + {file = "pynacl-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e"}, 1235 + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8"}, 1236 + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d"}, 1237 + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73"}, 1238 + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42"}, 1239 + {file = "pynacl-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4"}, 1240 + {file = "pynacl-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290"}, 1241 + {file = "pynacl-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:4853c154dc16ea12f8f3ee4b7e763331876316cc3a9f06aeedf39bcdca8f9995"}, 1242 + {file = "pynacl-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:347dcddce0b4d83ed3f32fd00379c83c425abee5a9d2cd0a2c84871334eaff64"}, 1243 + {file = "pynacl-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2d6cd56ce4998cb66a6c112fda7b1fdce5266c9f05044fa72972613bef376d15"}, 1244 + {file = "pynacl-1.6.0-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e"}, 1245 + {file = "pynacl-1.6.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990"}, 1246 + {file = "pynacl-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850"}, 1247 + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64"}, 1248 + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf"}, 1249 + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7"}, 1250 + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442"}, 1251 + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d"}, 1252 + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90"}, 1253 + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736"}, 1254 + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419"}, 1255 + {file = "pynacl-1.6.0-cp38-abi3-win32.whl", hash = "sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d"}, 1256 + {file = "pynacl-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1"}, 1257 + {file = "pynacl-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2"}, 1258 + {file = "pynacl-1.6.0.tar.gz", hash = "sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2"}, 1259 + ] 1260 + 1261 + [package.dependencies] 1262 + cffi = [ 1263 + {version = ">=1.4.1", markers = "platform_python_implementation != \"PyPy\" and python_version < \"3.14\""}, 1264 + {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""}, 1265 + ] 1266 + 1267 + [package.extras] 1268 + docs = ["sphinx (<7)", "sphinx_rtd_theme"] 1269 + tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] 1270 + 1271 + [[package]] 1272 + name = "pytest" 1273 + version = "8.4.2" 1274 + description = "pytest: simple powerful testing with Python" 1275 + optional = false 1276 + python-versions = ">=3.9" 1277 + groups = ["dev"] 1278 + files = [ 1279 + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, 1280 + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, 1281 + ] 1282 + 1283 + [package.dependencies] 1284 + colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 1285 + iniconfig = ">=1" 1286 + packaging = ">=20" 1287 + pluggy = ">=1.5,<2" 1288 + pygments = ">=2.7.2" 1289 + 1290 + [package.extras] 1291 + dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 1292 + 1293 + [[package]] 1294 + name = "pytest-cov" 1295 + version = "5.0.0" 1296 + description = "Pytest plugin for measuring coverage." 1297 + optional = false 1298 + python-versions = ">=3.8" 1299 + groups = ["dev"] 1300 + files = [ 1301 + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 1302 + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 1303 + ] 1304 + 1305 + [package.dependencies] 1306 + coverage = {version = ">=5.2.1", extras = ["toml"]} 1307 + pytest = ">=4.6" 1308 + 1309 + [package.extras] 1310 + testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 1311 + 1312 [[package]] 1313 name = "python-baseconv" 1314 version = "1.2.2" ··· 1326 description = "Python HTTP for Humans." 1327 optional = false 1328 python-versions = ">=3.9" 1329 + groups = ["main", "dev"] 1330 files = [ 1331 {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, 1332 {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ··· 1360 description = "Backported and Experimental Type Hints for Python 3.9+" 1361 optional = false 1362 python-versions = ">=3.9" 1363 + groups = ["main", "dev"] 1364 files = [ 1365 {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 1366 {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ··· 1387 description = "HTTP library with thread-safe connection pooling, file post, and more." 1388 optional = false 1389 python-versions = ">=3.9" 1390 + groups = ["main", "dev"] 1391 files = [ 1392 {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, 1393 {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ··· 1413 [metadata] 1414 lock-version = "2.1" 1415 python-versions = ">=3.13" 1416 + content-hash = "4124a2c3969985b1847d0e2ebf72c3bbe32c3a6fa870a0424c7c0e51ebb6e7f5"
+1
pyproject.toml
··· 26 [tool.poetry.group.dev.dependencies] 27 pytest = "^8.2.0" 28 pytest-cov = "^5.0.0" 29 30 31 [build-system]
··· 26 [tool.poetry.group.dev.dependencies] 27 pytest = "^8.2.0" 28 pytest-cov = "^5.0.0" 29 + pygithub = "^2.8.1" 30 31 32 [build-system]
+7
src/atpasser/model/converter.py
··· 15 UnknownModel, RecordModel, QueryModel, 16 ProcedureModel, SubscriptionModel 17 ) 18 19 class LexiconConverter: 20 """ ··· 31 "integer": IntegerModel, 32 "string": StringModel, 33 34 # Complex types 35 "array": ArrayModel, 36 "object": ObjectModel,
··· 15 UnknownModel, RecordModel, QueryModel, 16 ProcedureModel, SubscriptionModel 17 ) 18 + from .types.binary import BytesModel, CidLinkModel 19 + from .blob import BlobModel 20 21 class LexiconConverter: 22 """ ··· 33 "integer": IntegerModel, 34 "string": StringModel, 35 36 + # Binary types 37 + "bytes": BytesModel, 38 + "cid-link": CidLinkModel, 39 + "blob": BlobModel, 40 + 41 # Complex types 42 "array": ArrayModel, 43 "object": ObjectModel,
+214
src/atpasser/model/types/complex.py
···
··· 1 + from typing import Any 2 + from pydantic import field_validator 3 + from ..base import DataModel 4 + 5 + class ArrayModel(DataModel): 6 + """ 7 + Model for AT Protocol array type. 8 + 9 + Represents an array of elements with support for item schema definition, 10 + minimum/maximum length constraints as specified in Lexicon. 11 + """ 12 + 13 + items: Any 14 + """Schema definition for array elements""" 15 + 16 + minLength: int | None = None 17 + """Minimum number of elements""" 18 + 19 + maxLength: int | None = None 20 + """Maximum number of elements""" 21 + 22 + value: list[Any] 23 + """Array values""" 24 + 25 + def __init__(self, **data: Any) -> None: 26 + """ 27 + Initialize array model with validation. 28 + 29 + Args: 30 + **data: Input data containing array values 31 + 32 + Raises: 33 + ValueError: If array violates constraints 34 + """ 35 + super().__init__(**data) 36 + 37 + @field_validator("value", mode="before") 38 + def validate_array(cls, v: Any) -> list[Any]: 39 + """ 40 + Validate array structure and elements. 41 + 42 + Args: 43 + v: Value to validate 44 + 45 + Returns: 46 + Validated array 47 + 48 + Raises: 49 + ValueError: If array violates constraints 50 + """ 51 + if not isinstance(v, list): 52 + raise ValueError("Value must be an array") 53 + 54 + # Validate length constraints 55 + if cls.minLength is not None and len(v) < cls.minLength: 56 + raise ValueError(f"Array must have at least {cls.minLength} items") 57 + 58 + if cls.maxLength is not None and len(v) > cls.maxLength: 59 + raise ValueError(f"Array must have at most {cls.maxLength} items") 60 + 61 + return v 62 + 63 + class ObjectModel(DataModel): 64 + """ 65 + Model for AT Protocol object type. 66 + 67 + Represents a generic object schema with properties definitions, 68 + required fields and nullable fields as specified in Lexicon. 69 + """ 70 + 71 + properties: dict[str, Any] 72 + """Map of property names to their schema definitions""" 73 + 74 + required: list[str] | None = None 75 + """List of required property names""" 76 + 77 + nullable: list[str] | None = None 78 + """List of properties that can be null""" 79 + 80 + value: dict[str, Any] 81 + """Object property values""" 82 + 83 + def __init__(self, **data: Any) -> None: 84 + """ 85 + Initialize object model with validation. 86 + 87 + Args: 88 + **data: Input data containing object properties 89 + 90 + Raises: 91 + ValueError: If object violates constraints 92 + """ 93 + super().__init__(**data) 94 + 95 + @field_validator("value", mode="before") 96 + def validate_object(cls, v: Any) -> dict[str, Any]: 97 + """ 98 + Validate object structure and properties. 99 + 100 + Args: 101 + v: Value to validate 102 + 103 + Returns: 104 + Validated object 105 + 106 + Raises: 107 + ValueError: If object violates constraints 108 + """ 109 + if not isinstance(v, dict): 110 + raise ValueError("Value must be an object") 111 + 112 + # Validate required fields 113 + if cls.required: 114 + for field in cls.required: 115 + if field not in v: 116 + raise ValueError(f"Missing required field: {field}") 117 + 118 + # Validate nullable fields 119 + if cls.nullable: 120 + for field, value in v.items(): 121 + if field not in cls.nullable and value is None: 122 + raise ValueError(f"Field {field} cannot be null") 123 + 124 + return v 125 + 126 + class ParamsModel(DataModel): 127 + """ 128 + Model for AT Protocol params type. 129 + 130 + Specialized for HTTP query parameters with support for boolean, 131 + integer, string and unknown types as specified in Lexicon. 132 + """ 133 + 134 + required: list[str] | None = None 135 + """List of required parameter names""" 136 + 137 + properties: dict[str, Any] 138 + """Map of parameter names to their schema definitions""" 139 + 140 + value: dict[str, Any] 141 + """Parameter values 142 + 143 + Supported types: 144 + - boolean 145 + - integer 146 + - string 147 + - array (of boolean/integer/string/unknown) 148 + - unknown (object) 149 + """ 150 + 151 + def __init__(self, **data: Any) -> None: 152 + """ 153 + Initialize params model with validation. 154 + 155 + Args: 156 + **data: Input data containing parameter values 157 + 158 + Raises: 159 + ValueError: If parameters violate constraints 160 + """ 161 + super().__init__(**data) 162 + 163 + @field_validator("value", mode="before") 164 + def validate_params(cls, v: Any) -> dict[str, Any]: 165 + """ 166 + Validate parameters structure and values. 167 + 168 + Args: 169 + v: Value to validate 170 + 171 + Returns: 172 + Validated parameters 173 + 174 + Raises: 175 + ValueError: If parameters violate constraints 176 + """ 177 + if not isinstance(v, dict): 178 + raise ValueError("Value must be a dictionary of parameters") 179 + 180 + # Validate required parameters 181 + if cls.required: 182 + for param in cls.required: 183 + if param not in v: 184 + raise ValueError(f"Missing required parameter: {param}") 185 + 186 + # Validate parameter types 187 + for param, value in v.items(): 188 + if param in cls.properties: 189 + prop_type = cls.properties[param].get("type") 190 + if prop_type == "boolean" and not isinstance(value, bool): 191 + raise ValueError(f"Parameter {param} must be boolean") 192 + elif prop_type == "integer" and not isinstance(value, int): 193 + raise ValueError(f"Parameter {param} must be integer") 194 + elif prop_type == "string" and not isinstance(value, str): 195 + raise ValueError(f"Parameter {param} must be string") 196 + elif prop_type == "array": 197 + if not isinstance(value, list): 198 + raise ValueError(f"Parameter {param} must be array") 199 + # Validate array items if schema is specified 200 + if "items" in cls.properties[param]: 201 + item_type = cls.properties[param]["items"].get("type") 202 + for item in value: 203 + if item_type == "boolean" and not isinstance(item, bool): 204 + raise ValueError(f"Array item in {param} must be boolean") 205 + elif item_type == "integer" and not isinstance(item, int): 206 + raise ValueError(f"Array item in {param} must be integer") 207 + elif item_type == "string" and not isinstance(item, str): 208 + raise ValueError(f"Array item in {param} must be string") 209 + elif item_type == "unknown" and not isinstance(item, dict): 210 + raise ValueError(f"Array item in {param} must be object") 211 + elif prop_type == "unknown" and not isinstance(value, dict): 212 + raise ValueError(f"Parameter {param} must be object") 213 + 214 + return v
+172
src/atpasser/model/types/primitive.py
···
··· 1 + from typing import Any 2 + from pydantic import field_validator 3 + from ..base import DataModel 4 + 5 + class NullModel(DataModel): 6 + """ 7 + Model for AT Protocol null type. 8 + 9 + Represents a null value in AT Protocol data model. This model ensures proper 10 + serialization and validation of null values according to Lexicon specification. 11 + """ 12 + 13 + value: None = None 14 + """Always None for null type""" 15 + 16 + def __init__(self, **data: Any) -> None: 17 + """ 18 + Initialize null model with validation. 19 + 20 + Args: 21 + **data: Input data (must be empty or contain only None values) 22 + 23 + Raises: 24 + ValueError: If non-null value is provided 25 + """ 26 + if data and any(v is not None for v in data.values()): 27 + raise ValueError("NullModel only accepts None values") 28 + super().__init__(**data) 29 + 30 + @field_validator("*", mode="before") 31 + def validate_null(cls, v: Any) -> None: 32 + """ 33 + Validate that value is null. 34 + 35 + Args: 36 + v: Value to validate 37 + 38 + Returns: 39 + None if validation succeeds 40 + 41 + Raises: 42 + ValueError: If value is not null 43 + """ 44 + if v is not None: 45 + raise ValueError("NullModel only accepts None values") 46 + return None 47 + 48 + class BooleanModel(DataModel): 49 + """ 50 + Model for AT Protocol boolean type. 51 + 52 + Represents a boolean value in AT Protocol data model with support for 53 + default values and constants as specified in Lexicon. 54 + """ 55 + 56 + value: bool 57 + """Boolean value""" 58 + 59 + default: bool | None = None 60 + """Default value if not provided""" 61 + 62 + const: bool | None = None 63 + """Fixed constant value if specified""" 64 + 65 + def __init__(self, **data: Any) -> None: 66 + """ 67 + Initialize boolean model with validation. 68 + 69 + Args: 70 + **data: Input data containing boolean value 71 + 72 + Raises: 73 + ValueError: If value doesn't match const or is not boolean 74 + """ 75 + super().__init__(**data) 76 + if self.const is not None and self.value != self.const: 77 + raise ValueError(f"Boolean value must be {self.const}") 78 + 79 + @field_validator("value", mode="before") 80 + def validate_boolean(cls, v: Any) -> bool: 81 + """ 82 + Validate and convert input to boolean. 83 + 84 + Args: 85 + v: Value to validate 86 + 87 + Returns: 88 + Validated boolean value 89 + 90 + Raises: 91 + ValueError: If value cannot be converted to boolean 92 + """ 93 + if isinstance(v, bool): 94 + return v 95 + if isinstance(v, str): 96 + if v.lower() in ("true", "1"): 97 + return True 98 + if v.lower() in ("false", "0"): 99 + return False 100 + raise ValueError("Value must be a boolean") 101 + 102 + class IntegerModel(DataModel): 103 + """ 104 + Model for AT Protocol integer type. 105 + 106 + Represents a signed integer number with support for minimum/maximum values, 107 + enumeration sets, default values and constraints as specified in Lexicon. 108 + """ 109 + 110 + value: int 111 + """Integer value""" 112 + 113 + minimum: int | None = None 114 + """Minimum acceptable value""" 115 + 116 + maximum: int | None = None 117 + """Maximum acceptable value""" 118 + 119 + enum: list[int] | None = None 120 + """Closed set of allowed values""" 121 + 122 + default: int | None = None 123 + """Default value if not provided""" 124 + 125 + const: int | None = None 126 + """Fixed constant value if specified""" 127 + 128 + def __init__(self, **data: Any) -> None: 129 + """ 130 + Initialize integer model with validation. 131 + 132 + Args: 133 + **data: Input data containing integer value 134 + 135 + Raises: 136 + ValueError: If value violates constraints 137 + """ 138 + super().__init__(**data) 139 + if self.const is not None and self.value != self.const: 140 + raise ValueError(f"Integer value must be {self.const}") 141 + 142 + @field_validator("value", mode="before") 143 + def validate_integer(cls, v: Any) -> int: 144 + """ 145 + Validate and convert input to integer. 146 + 147 + Args: 148 + v: Value to validate 149 + 150 + Returns: 151 + Validated integer value 152 + 153 + Raises: 154 + ValueError: If value violates constraints 155 + """ 156 + if not isinstance(v, int): 157 + try: 158 + v = int(v) 159 + except (TypeError, ValueError): 160 + raise ValueError("Value must be an integer") 161 + 162 + # Validate against instance attributes 163 + if cls.enum and v not in cls.enum: 164 + raise ValueError(f"Value must be one of {cls.enum}") 165 + 166 + if cls.minimum is not None and v < cls.minimum: 167 + raise ValueError(f"Value must be >= {cls.minimum}") 168 + 169 + if cls.maximum is not None and v > cls.maximum: 170 + raise ValueError(f"Value must be <= {cls.maximum}") 171 + 172 + return v
+131
src/atpasser/model/types/reference.py
···
··· 1 + from typing import Any 2 + from pydantic import field_validator 3 + from ..base import DataModel 4 + 5 + class TokenModel(DataModel): 6 + """ 7 + Model for AT Protocol token type. 8 + 9 + Represents empty data values which exist only to be referenced by name. 10 + Tokens encode as string data with the string being the fully-qualified 11 + reference to the token itself (NSID followed by optional fragment). 12 + """ 13 + 14 + name: str 15 + """Token name/identifier""" 16 + 17 + description: str | None = None 18 + """Description clarifying the meaning of the token""" 19 + 20 + def __init__(self, **data: Any) -> None: 21 + """ 22 + Initialize token model. 23 + 24 + Args: 25 + **data: Input data containing token name 26 + """ 27 + super().__init__(**data) 28 + 29 + @field_validator("name") 30 + def validate_name(cls, v: str) -> str: 31 + """ 32 + Validate token name format. 33 + 34 + Args: 35 + v: Name to validate 36 + 37 + Returns: 38 + Validated name 39 + 40 + Raises: 41 + ValueError: If name contains whitespace 42 + """ 43 + if any(c.isspace() for c in v): 44 + raise ValueError("Token name must not contain whitespace") 45 + return v 46 + 47 + class RefModel(DataModel): 48 + """ 49 + Model for AT Protocol ref type. 50 + 51 + Represents a reference to another schema definition, either globally 52 + (using NSID) or locally (using #-delimited name). 53 + """ 54 + 55 + ref: str 56 + """Reference to schema definition (NSID or #name)""" 57 + 58 + description: str | None = None 59 + """Description of the reference""" 60 + 61 + def __init__(self, **data: Any) -> None: 62 + """ 63 + Initialize reference model. 64 + 65 + Args: 66 + **data: Input data containing reference 67 + """ 68 + super().__init__(**data) 69 + 70 + @field_validator("ref") 71 + def validate_ref(cls, v: str) -> str: 72 + """ 73 + Validate reference format. 74 + 75 + Args: 76 + v: Reference to validate 77 + 78 + Returns: 79 + Validated reference 80 + 81 + Raises: 82 + ValueError: If reference is empty or invalid 83 + """ 84 + if not v: 85 + raise ValueError("Reference cannot be empty") 86 + return v 87 + 88 + class UnionModel(DataModel): 89 + """ 90 + Model for AT Protocol union type. 91 + 92 + Represents that multiple possible types could be present at a location. 93 + The references follow the same syntax as `ref`, allowing references to 94 + both global or local schema definitions. 95 + """ 96 + 97 + refs: list[str] 98 + """References to schema definitions""" 99 + 100 + closed: bool = False 101 + """Indicates if union is open (can be extended) or closed""" 102 + 103 + description: str | None = None 104 + """Description of the union""" 105 + 106 + def __init__(self, **data: Any) -> None: 107 + """ 108 + Initialize union model. 109 + 110 + Args: 111 + **data: Input data containing union references 112 + """ 113 + super().__init__(**data) 114 + 115 + @field_validator("refs") 116 + def validate_refs(cls, v: list[str]) -> list[str]: 117 + """ 118 + Validate union references. 119 + 120 + Args: 121 + v: References to validate 122 + 123 + Returns: 124 + Validated references 125 + 126 + Raises: 127 + ValueError: If references list is empty for closed union 128 + """ 129 + if cls.closed and not v: 130 + raise ValueError("Closed union must have at least one reference") 131 + return v
+323
src/atpasser/model/types/special.py
···
··· 1 + from typing import Any 2 + from pydantic import field_validator 3 + from ..base import DataModel 4 + 5 + class UnknownModel(DataModel): 6 + """ 7 + Model for AT Protocol unknown type. 8 + 9 + Indicates that any data object could appear at this location, 10 + with no specific validation. The top-level data must be an object. 11 + """ 12 + 13 + description: str | None = None 14 + """Description of the unknown type usage""" 15 + 16 + def __init__(self, **data: Any) -> None: 17 + """ 18 + Initialize unknown model. 19 + 20 + Args: 21 + **data: Input data containing unknown object 22 + """ 23 + super().__init__(**data) 24 + 25 + @field_validator("*", mode="before") 26 + def validate_unknown(cls, v: Any) -> Any: 27 + """ 28 + Validate unknown data is an object. 29 + 30 + Args: 31 + v: Value to validate 32 + 33 + Returns: 34 + Validated value 35 + 36 + Raises: 37 + ValueError: If value is not an object 38 + """ 39 + if not isinstance(v, dict): 40 + raise ValueError("Unknown type must be an object") 41 + return v 42 + 43 + class RecordModel(DataModel): 44 + """ 45 + Model for AT Protocol record type. 46 + 47 + Describes an object that can be stored in a repository record. 48 + Records must include a $type field indicating their schema. 49 + """ 50 + 51 + key: str 52 + """Specifies the Record Key type""" 53 + 54 + record: dict[str, Any] 55 + """Schema definition with type 'object'""" 56 + 57 + type: str 58 + """Lexicon schema type identifier""" 59 + 60 + def __init__(self, **data: Any) -> None: 61 + """ 62 + Initialize record model with validation. 63 + 64 + Args: 65 + **data: Input data containing record values 66 + 67 + Raises: 68 + ValueError: If record is missing required fields 69 + """ 70 + # Extract $type if present 71 + data_type = data.pop("$type", None) 72 + if data_type: 73 + data["type"] = data_type 74 + super().__init__(**data) 75 + 76 + @field_validator("type") 77 + def validate_type(cls, v: str) -> str: 78 + """ 79 + Validate record type field. 80 + 81 + Args: 82 + v: Type value to validate 83 + 84 + Returns: 85 + Validated type 86 + 87 + Raises: 88 + ValueError: If type is empty 89 + """ 90 + if not v: 91 + raise ValueError("Record must have a type") 92 + return v 93 + 94 + @field_validator("record", mode="before") 95 + def validate_record(cls, v: Any) -> dict[str, Any]: 96 + """ 97 + Validate record structure. 98 + 99 + Args: 100 + v: Record value to validate 101 + 102 + Returns: 103 + Validated record 104 + 105 + Raises: 106 + ValueError: If record is not an object 107 + """ 108 + if not isinstance(v, dict): 109 + raise ValueError("Record must be an object") 110 + return v 111 + 112 + class QueryModel(DataModel): 113 + """ 114 + Model for AT Protocol query type. 115 + 116 + Describes an XRPC Query endpoint (HTTP GET) with support for 117 + parameters, output schema and error responses. 118 + """ 119 + 120 + parameters: dict[str, Any] | None = None 121 + """HTTP query parameters schema""" 122 + 123 + output: dict[str, Any] | None = None 124 + """HTTP response body schema""" 125 + 126 + errors: list[dict[str, str]] | None = None 127 + """Possible error responses""" 128 + 129 + def __init__(self, **data: Any) -> None: 130 + """ 131 + Initialize query model with validation. 132 + 133 + Args: 134 + **data: Input data containing query definition 135 + """ 136 + super().__init__(**data) 137 + 138 + @field_validator("output") 139 + def validate_output(cls, v: dict[str, Any] | None) -> dict[str, Any] | None: 140 + """ 141 + Validate output schema. 142 + 143 + Args: 144 + v: Output schema to validate 145 + 146 + Returns: 147 + Validated output schema 148 + 149 + Raises: 150 + ValueError: If output schema is invalid 151 + """ 152 + if v and "encoding" not in v: 153 + raise ValueError("Output must specify encoding") 154 + return v 155 + 156 + @field_validator("errors") 157 + def validate_errors(cls, v: list[dict[str, str]] | None) -> list[dict[str, str]] | None: 158 + """ 159 + Validate error definitions. 160 + 161 + Args: 162 + v: Error definitions to validate 163 + 164 + Returns: 165 + Validated error definitions 166 + 167 + Raises: 168 + ValueError: If any error definition is invalid 169 + """ 170 + if v: 171 + for error in v: 172 + if "name" not in error: 173 + raise ValueError("Error must have a name") 174 + return v 175 + 176 + class ProcedureModel(DataModel): 177 + """ 178 + Model for AT Protocol procedure type. 179 + 180 + Describes an XRPC Procedure endpoint (HTTP POST) with support for 181 + parameters, input/output schemas and error responses. 182 + """ 183 + 184 + parameters: dict[str, Any] | None = None 185 + """HTTP query parameters schema""" 186 + 187 + input: dict[str, Any] | None = None 188 + """HTTP request body schema""" 189 + 190 + output: dict[str, Any] | None = None 191 + """HTTP response body schema""" 192 + 193 + errors: list[dict[str, str]] | None = None 194 + """Possible error responses""" 195 + 196 + def __init__(self, **data: Any) -> None: 197 + """ 198 + Initialize procedure model with validation. 199 + 200 + Args: 201 + **data: Input data containing procedure definition 202 + """ 203 + super().__init__(**data) 204 + 205 + @field_validator("input") 206 + def validate_input(cls, v: dict[str, Any] | None) -> dict[str, Any] | None: 207 + """ 208 + Validate input schema. 209 + 210 + Args: 211 + v: Input schema to validate 212 + 213 + Returns: 214 + Validated input schema 215 + 216 + Raises: 217 + ValueError: If input schema is invalid 218 + """ 219 + if v and "encoding" not in v: 220 + raise ValueError("Input must specify encoding") 221 + return v 222 + 223 + @field_validator("output") 224 + def validate_output(cls, v: dict[str, Any] | None) -> dict[str, Any] | None: 225 + """ 226 + Validate output schema. 227 + 228 + Args: 229 + v: Output schema to validate 230 + 231 + Returns: 232 + Validated output schema 233 + 234 + Raises: 235 + ValueError: If output schema is invalid 236 + """ 237 + if v and "encoding" not in v: 238 + raise ValueError("Output must specify encoding") 239 + return v 240 + 241 + @field_validator("errors") 242 + def validate_errors(cls, v: list[dict[str, str]] | None) -> list[dict[str, str]] | None: 243 + """ 244 + Validate error definitions. 245 + 246 + Args: 247 + v: Error definitions to validate 248 + 249 + Returns: 250 + Validated error definitions 251 + 252 + Raises: 253 + ValueError: If any error definition is invalid 254 + """ 255 + if v: 256 + for error in v: 257 + if "name" not in error: 258 + raise ValueError("Error must have a name") 259 + return v 260 + 261 + class SubscriptionModel(DataModel): 262 + """ 263 + Model for AT Protocol subscription type. 264 + 265 + Describes an Event Stream (WebSocket) with support for parameters, 266 + message schemas and error responses. 267 + """ 268 + 269 + parameters: dict[str, Any] | None = None 270 + """HTTP query parameters schema""" 271 + 272 + message: dict[str, Any] | None = None 273 + """Specifies what messages can be""" 274 + 275 + errors: list[dict[str, str]] | None = None 276 + """Possible error responses""" 277 + 278 + def __init__(self, **data: Any) -> None: 279 + """ 280 + Initialize subscription model with validation. 281 + 282 + Args: 283 + **data: Input data containing subscription definition 284 + """ 285 + super().__init__(**data) 286 + 287 + @field_validator("message") 288 + def validate_message(cls, v: dict[str, Any] | None) -> dict[str, Any] | None: 289 + """ 290 + Validate message schema. 291 + 292 + Args: 293 + v: Message schema to validate 294 + 295 + Returns: 296 + Validated message schema 297 + 298 + Raises: 299 + ValueError: If message schema is invalid 300 + """ 301 + if v and "schema" not in v: 302 + raise ValueError("Message must specify schema") 303 + return v 304 + 305 + @field_validator("errors") 306 + def validate_errors(cls, v: list[dict[str, str]] | None) -> list[dict[str, str]] | None: 307 + """ 308 + Validate error definitions. 309 + 310 + Args: 311 + v: Error definitions to validate 312 + 313 + Returns: 314 + Validated error definitions 315 + 316 + Raises: 317 + ValueError: If any error definition is invalid 318 + """ 319 + if v: 320 + for error in v: 321 + if "name" not in error: 322 + raise ValueError("Error must have a name") 323 + return v
+249
src/atpasser/model/types/string.py
···
··· 1 + from typing import Any 2 + import re 3 + from datetime import datetime 4 + from pydantic import field_validator 5 + from ..base import DataModel 6 + 7 + class StringModel(DataModel): 8 + """ 9 + Model for AT Protocol string type. 10 + 11 + Represents a Unicode string with support for format restrictions, length limits, 12 + known values, enumeration sets, default values and constants as specified in Lexicon. 13 + """ 14 + 15 + value: str 16 + """String value""" 17 + 18 + format: str | None = None 19 + """String format restriction (e.g. 'datetime', 'uri')""" 20 + 21 + maxLength: int | None = None 22 + """Maximum length in UTF-8 bytes""" 23 + 24 + minLength: int | None = None 25 + """Minimum length in UTF-8 bytes""" 26 + 27 + knownValues: list[str] | None = None 28 + """Suggested/common values (not enforced)""" 29 + 30 + enum: list[str] | None = None 31 + """Closed set of allowed values""" 32 + 33 + default: str | None = None 34 + """Default value if not provided""" 35 + 36 + const: str | None = None 37 + """Fixed constant value if specified""" 38 + 39 + def __init__(self, **data: Any) -> None: 40 + """ 41 + Initialize string model with validation. 42 + 43 + Args: 44 + **data: Input data containing string value 45 + 46 + Raises: 47 + ValueError: If value violates constraints 48 + """ 49 + super().__init__(**data) 50 + if self.const is not None and self.value != self.const: 51 + raise ValueError(f"String value must be {self.const}") 52 + 53 + @field_validator("value", mode="before") 54 + def validate_string(cls, v: Any) -> str: 55 + """ 56 + Validate and convert input to string. 57 + 58 + Args: 59 + v: Value to validate 60 + 61 + Returns: 62 + Validated string value 63 + 64 + Raises: 65 + ValueError: If value violates constraints 66 + """ 67 + if not isinstance(v, str): 68 + v = str(v) 69 + 70 + # Validate length constraints 71 + if cls.minLength is not None and len(v.encode()) < cls.minLength: 72 + raise ValueError(f"String must be at least {cls.minLength} bytes") 73 + 74 + if cls.maxLength is not None and len(v.encode()) > cls.maxLength: 75 + raise ValueError(f"String must be at most {cls.maxLength} bytes") 76 + 77 + # Validate enum 78 + if cls.enum and v not in cls.enum: 79 + raise ValueError(f"Value must be one of {cls.enum}") 80 + 81 + # Validate format if specified 82 + if cls.format: 83 + if cls.format == "datetime": 84 + cls._validate_datetime(v) 85 + elif cls.format == "uri": 86 + cls._validate_uri(v) 87 + elif cls.format == "did": 88 + cls._validate_did(v) 89 + elif cls.format == "handle": 90 + cls._validate_handle(v) 91 + elif cls.format == "at-identifier": 92 + cls._validate_at_identifier(v) 93 + elif cls.format == "at-uri": 94 + cls._validate_at_uri(v) 95 + elif cls.format == "cid": 96 + cls._validate_cid(v) 97 + elif cls.format == "nsid": 98 + cls._validate_nsid(v) 99 + elif cls.format == "tid": 100 + cls._validate_tid(v) 101 + elif cls.format == "record-key": 102 + cls._validate_record_key(v) 103 + elif cls.format == "language": 104 + cls._validate_language(v) 105 + 106 + return v 107 + 108 + @classmethod 109 + def _validate_datetime(cls, v: str) -> None: 110 + """Validate RFC 3339 datetime format""" 111 + try: 112 + datetime.fromisoformat(v.replace("Z", "+00:00")) 113 + except ValueError: 114 + raise ValueError("Invalid datetime format, must be RFC 3339") 115 + 116 + @classmethod 117 + def _validate_uri(cls, v: str) -> None: 118 + """Validate URI format""" 119 + if len(v) > 8192: # 8KB max 120 + raise ValueError("URI too long, max 8KB") 121 + if not re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:.+", v): 122 + raise ValueError("Invalid URI format") 123 + 124 + @classmethod 125 + def _validate_did(cls, v: str) -> None: 126 + """Validate DID format""" 127 + if len(v) > 2048: 128 + raise ValueError("DID too long, max 2048 chars") 129 + if not re.match(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$", v): 130 + raise ValueError("Invalid URI format") 131 + 132 + @classmethod 133 + def _validate_handle(cls, v: str) -> None: 134 + """Validate handle format""" 135 + if not re.match(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", v): 136 + raise ValueError("Handle contains invalid characters") 137 + if len(v) > 253: 138 + raise ValueError("Handle too long, max 253 chars") 139 + 140 + @classmethod 141 + def _validate_at_identifier(cls, v: str) -> None: 142 + """Validate at-identifier format (DID or handle)""" 143 + try: 144 + if v.startswith("did:"): 145 + cls._validate_did(v) 146 + else: 147 + cls._validate_handle(v) 148 + except ValueError as e: 149 + raise ValueError(f"Invalid at-identifier: {e}") 150 + 151 + @classmethod 152 + def _validate_at_uri(cls, v: str) -> None: 153 + """ 154 + Validate AT-URI format according to AT Protocol specification. 155 + 156 + Args: 157 + v: AT-URI string to validate 158 + 159 + Raises: 160 + ValueError: If URI violates any of these rules: 161 + - Must start with 'at://' 162 + - Max length 8KB 163 + - No trailing slash 164 + - Authority must be valid DID or handle 165 + - Path segments must follow NSID/RKEY rules if present 166 + """ 167 + if not v.startswith("at://"): 168 + raise ValueError("AT-URI must start with 'at://'") 169 + if len(v) > 8192: # 8KB 170 + raise ValueError("AT-URI too long, max 8KB") 171 + if v.endswith('/'): 172 + raise ValueError("AT-URI cannot have trailing slash") 173 + 174 + # Split into parts 175 + parts = v[5:].split('/') # Skip 'at://' 176 + authority = parts[0] 177 + 178 + # Validate authority (DID or handle) 179 + if not authority: 180 + raise ValueError("AT-URI must have authority") 181 + 182 + if authority.startswith('did:'): 183 + # Basic DID format check - actual DID validation is done elsewhere 184 + if len(authority) > 2048: 185 + raise ValueError("DID too long") 186 + if ':' not in authority[4:]: 187 + raise ValueError("Invalid DID format") 188 + else: 189 + # Handle validation 190 + if not re.match(r'^[a-z0-9.-]+$', authority): 191 + raise ValueError("Invalid handle characters") 192 + if len(authority) > 253: 193 + raise ValueError("Handle too long") 194 + 195 + # Validate path segments if present 196 + if len(parts) > 1: 197 + if len(parts) > 3: 198 + raise ValueError("AT-URI path too deep") 199 + 200 + collection = parts[1] 201 + if not re.match(r'^[a-zA-Z0-9.-]+$', collection): 202 + raise ValueError("Invalid collection NSID") 203 + 204 + if len(parts) > 2: 205 + rkey = parts[2] 206 + if not rkey: 207 + raise ValueError("Record key cannot be empty") 208 + if not re.match(r'^[a-zA-Z0-9._:%-~]+$', rkey): 209 + raise ValueError("Invalid record key characters") 210 + 211 + @classmethod 212 + def _validate_cid(cls, v: str) -> None: 213 + """Validate CID string format""" 214 + if len(v) > 100: 215 + raise ValueError("CID too long, max 100 chars") 216 + if not re.match(r"^[a-zA-Z0-9]+$", v): 217 + raise ValueError("CID contains invalid characters") 218 + 219 + @classmethod 220 + def _validate_nsid(cls, v: str) -> None: 221 + """Validate NSID format""" 222 + if len(v) > 317: 223 + raise ValueError("NSID too long, max 317 chars") 224 + if not re.match(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$", v): 225 + raise ValueError("NSID contains invalid characters") 226 + 227 + @classmethod 228 + def _validate_tid(cls, v: str) -> None: 229 + """Validate TID format""" 230 + if len(v) > 13: 231 + raise ValueError("TID too long, max 13 chars") 232 + if not re.match(r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$", v): 233 + raise ValueError("TID contains invalid characters") 234 + 235 + @classmethod 236 + def _validate_record_key(cls, v: str) -> None: 237 + """Validate record-key format""" 238 + if len(v) > 512: 239 + raise ValueError("Record key too long, max 512 chars") 240 + if v == "." or v == "..": 241 + raise ValueError(f"Record key is {v}, which is not allowed") 242 + if not re.match(r"^[a-zA-Z0-9._:%-~]+$", v): 243 + raise ValueError("Record key contains invalid characters") 244 + 245 + @classmethod 246 + def _validate_language(cls, v: str) -> None: 247 + """Validate BCP 47 language tag""" 248 + if not re.match(r"^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$", v): 249 + raise ValueError("Invalid language tag format")