+41
src/atpasser/model/typed.py
+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
+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
+3
src/atpasser/model/__init__.py
···
38
38
ProcedureModel,
39
39
SubscriptionModel
40
40
)
41
+
from .converter import LexiconConverter
41
42
42
43
__all__ = [
43
44
"DataModel",
···
61
62
"QueryModel",
62
63
"ProcedureModel",
63
64
"SubscriptionModel",
65
+
# Converter
66
+
"LexiconConverter",
64
67
# Exceptions
65
68
"AtprotoModelError",
66
69
"ValidationError",
-5
src/atpasser/model/base.py
-5
src/atpasser/model/base.py
···
1
1
import base64
2
-
import re
3
-
from datetime import datetime
4
2
from typing import Any
5
-
from collections.abc import Mapping
6
3
from cid.cid import CIDv1, make_cid
7
4
from pydantic import BaseModel, field_serializer, field_validator, ConfigDict
8
-
from pydantic.fields import FieldInfo
9
5
from .exceptions import (
10
-
ValidationError,
11
6
SerializationError,
12
7
DeserializationError,
13
8
InvalidCIDError
+5
-5
src/atpasser/model/blob.py
+5
-5
src/atpasser/model/blob.py
···
1
1
from typing import Any
2
2
from pydantic import field_validator, ConfigDict
3
3
from .base import DataModel
4
-
from .exceptions import ValidationError, InvalidBlobError
4
+
from .exceptions import ValidationError
5
5
6
6
class BlobModel(DataModel):
7
7
"""
···
34
34
Validated size
35
35
36
36
Raises:
37
-
ValueError: If size is not positive
37
+
ValidationError: If size is not positive
38
38
"""
39
39
if v <= 0:
40
-
raise ValueError("Blob size must be positive and non-zero")
40
+
raise ValidationError(field="size", message="must be positive and non-zero")
41
41
return v
42
42
43
43
@field_validator("mimeType")
···
52
52
Validated MIME type
53
53
54
54
Raises:
55
-
ValueError: If MIME type is empty
55
+
ValidationError: If MIME type is empty
56
56
"""
57
57
if not v:
58
-
raise ValueError("MIME type cannot be empty")
58
+
raise ValidationError(field="mimeType", message="cannot be empty")
59
59
return v
+323
-10
poetry.lock
+323
-10
poetry.lock
···
103
103
description = "Python package for providing Mozilla's CA Bundle."
104
104
optional = false
105
105
python-versions = ">=3.7"
106
-
groups = ["main"]
106
+
groups = ["main", "dev"]
107
107
files = [
108
108
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
109
109
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
···
115
115
description = "Foreign Function Interface for Python calling C code."
116
116
optional = false
117
117
python-versions = ">=3.9"
118
-
groups = ["main"]
118
+
groups = ["main", "dev"]
119
119
markers = "platform_python_implementation != \"PyPy\""
120
120
files = [
121
121
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
···
213
213
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
214
214
optional = false
215
215
python-versions = ">=3.7"
216
-
groups = ["main"]
216
+
groups = ["main", "dev"]
217
217
files = [
218
218
{file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"},
219
219
{file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"},
···
296
296
{file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"},
297
297
]
298
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
+
299
429
[[package]]
300
430
name = "cryptography"
301
431
version = "45.0.7"
302
432
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
303
433
optional = false
304
434
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
305
-
groups = ["main"]
435
+
groups = ["main", "dev"]
306
436
files = [
307
437
{file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"},
308
438
{file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"},
···
432
562
description = "Internationalized Domain Names in Applications (IDNA)"
433
563
optional = false
434
564
python-versions = ">=3.6"
435
-
groups = ["main"]
565
+
groups = ["main", "dev"]
436
566
files = [
437
567
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
438
568
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
···
441
571
[package.extras]
442
572
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
443
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
+
444
586
[[package]]
445
587
name = "jsonpath-ng"
446
588
version = "1.7.0"
···
740
882
{file = "morphys-1.0-py2.py3-none-any.whl", hash = "sha256:76d6dbaa4d65f597e59d332c81da786d83e4669387b9b2a750cfec74e7beec20"},
741
883
]
742
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
+
743
913
[[package]]
744
914
name = "ply"
745
915
version = "3.11"
···
815
985
description = "C parser in Python"
816
986
optional = false
817
987
python-versions = ">=3.8"
818
-
groups = ["main"]
988
+
groups = ["main", "dev"]
819
989
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
820
990
files = [
821
991
{file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
···
956
1126
[package.dependencies]
957
1127
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
958
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
+
959
1184
[[package]]
960
1185
name = "pyld"
961
1186
version = "2.0.4"
···
996
1221
blake2 = ["pyblake2"]
997
1222
sha3 = ["pysha3"]
998
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
+
999
1312
[[package]]
1000
1313
name = "python-baseconv"
1001
1314
version = "1.2.2"
···
1013
1326
description = "Python HTTP for Humans."
1014
1327
optional = false
1015
1328
python-versions = ">=3.9"
1016
-
groups = ["main"]
1329
+
groups = ["main", "dev"]
1017
1330
files = [
1018
1331
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
1019
1332
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
···
1047
1360
description = "Backported and Experimental Type Hints for Python 3.9+"
1048
1361
optional = false
1049
1362
python-versions = ">=3.9"
1050
-
groups = ["main"]
1363
+
groups = ["main", "dev"]
1051
1364
files = [
1052
1365
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
1053
1366
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
···
1074
1387
description = "HTTP library with thread-safe connection pooling, file post, and more."
1075
1388
optional = false
1076
1389
python-versions = ">=3.9"
1077
-
groups = ["main"]
1390
+
groups = ["main", "dev"]
1078
1391
files = [
1079
1392
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
1080
1393
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
···
1100
1413
[metadata]
1101
1414
lock-version = "2.1"
1102
1415
python-versions = ">=3.13"
1103
-
content-hash = "4919ab150fee9e4e358e57bada62225cb2d92c52509b26169db269691b86cefe"
1416
+
content-hash = "4124a2c3969985b1847d0e2ebf72c3bbe32c3a6fa870a0424c7c0e51ebb6e7f5"
+1
pyproject.toml
+1
pyproject.toml
+7
src/atpasser/model/converter.py
+7
src/atpasser/model/converter.py
···
15
15
UnknownModel, RecordModel, QueryModel,
16
16
ProcedureModel, SubscriptionModel
17
17
)
18
+
from .types.binary import BytesModel, CidLinkModel
19
+
from .blob import BlobModel
18
20
19
21
class LexiconConverter:
20
22
"""
···
31
33
"integer": IntegerModel,
32
34
"string": StringModel,
33
35
36
+
# Binary types
37
+
"bytes": BytesModel,
38
+
"cid-link": CidLinkModel,
39
+
"blob": BlobModel,
40
+
34
41
# Complex types
35
42
"array": ArrayModel,
36
43
"object": ObjectModel,
+214
src/atpasser/model/types/complex.py
+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
+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
+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
+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
+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")