-703
src/atpasser/model/types.py
-703
src/atpasser/model/types.py
···
1
-
"""
2
-
AT Protocol Lexicon Type Models
3
-
4
-
Combined implementation of all AT Protocol Lexicon data types including:
5
-
- Primitive types (boolean, integer, string, null)
6
-
- Complex types (array, object, params)
7
-
- Reference types (ref, union, token)
8
-
- Special types (record, query, procedure, subscription)
9
-
- Binary types (bytes, CID links)
10
-
"""
11
-
12
-
from typing import Any
13
-
import re
14
-
import base64
15
-
from datetime import datetime
16
-
from pydantic import field_validator, field_serializer
17
-
from cid.cid import CIDv1, make_cid
18
-
19
-
from .base import DataModel
20
-
from .exceptions import ValidationError, SerializationError, InvalidCIDError
21
-
22
-
23
-
class BooleanModel(DataModel):
24
-
"""Model for AT Protocol boolean type."""
25
-
26
-
value: bool
27
-
"""Boolean value"""
28
-
29
-
default: bool | None = None
30
-
"""Default value if not provided"""
31
-
32
-
const: bool | None = None
33
-
"""Fixed constant value if specified"""
34
-
35
-
def __init__(self, **data: Any) -> None:
36
-
"""
37
-
Initialize boolean model with validation.
38
-
39
-
Args:
40
-
**data: Input data containing boolean value
41
-
42
-
Raises:
43
-
ValueError: If value doesn't match const or is not boolean
44
-
"""
45
-
super().__init__(**data)
46
-
if self.const is not None and self.value != self.const:
47
-
raise ValueError(f"Boolean value must be {self.const}")
48
-
49
-
@field_validator("value", mode="before")
50
-
def validateBoolean(cls, v: Any) -> bool:
51
-
"""
52
-
Validate and convert input to boolean.
53
-
54
-
Args:
55
-
v: Value to validate
56
-
57
-
Returns:
58
-
Validated boolean value
59
-
60
-
Raises:
61
-
ValueError: If value cannot be converted to boolean
62
-
"""
63
-
if isinstance(v, bool):
64
-
return v
65
-
if isinstance(v, str):
66
-
if v.lower() in ("true", "1"):
67
-
return True
68
-
if v.lower() in ("false", "0"):
69
-
return False
70
-
raise ValueError("Value must be a boolean")
71
-
72
-
73
-
class IntegerModel(DataModel):
74
-
"""Model for AT Protocol integer type."""
75
-
76
-
value: int
77
-
"""Integer value"""
78
-
79
-
minimum: int | None = None
80
-
"""Minimum acceptable value"""
81
-
82
-
maximum: int | None = None
83
-
"""Maximum acceptable value"""
84
-
85
-
enum: list[int] | None = None
86
-
"""Closed set of allowed values"""
87
-
88
-
default: int | None = None
89
-
"""Default value if not provided"""
90
-
91
-
const: int | None = None
92
-
"""Fixed constant value if specified"""
93
-
94
-
def __init__(self, **data: Any) -> None:
95
-
"""
96
-
Initialize integer model with validation.
97
-
98
-
Args:
99
-
**data: Input data containing integer value
100
-
101
-
Raises:
102
-
ValueError: If value violates constraints
103
-
"""
104
-
super().__init__(**data)
105
-
if self.const is not None and self.value != self.const:
106
-
raise ValueError(f"Integer value must be {self.const}")
107
-
108
-
@field_validator("value", mode="before")
109
-
def validateInteger(cls, v: Any) -> int:
110
-
"""
111
-
Validate and convert input to integer.
112
-
113
-
Args:
114
-
v: Value to validate
115
-
116
-
Returns:
117
-
Validated integer value
118
-
119
-
Raises:
120
-
ValueError: If value violates constraints
121
-
"""
122
-
if not isinstance(v, int):
123
-
try:
124
-
v = int(v)
125
-
except (TypeError, ValueError):
126
-
raise ValueError("Value must be an integer")
127
-
128
-
if cls.enum and v not in cls.enum:
129
-
raise ValueError(f"Value must be one of {cls.enum}")
130
-
131
-
if cls.minimum is not None and v < cls.minimum:
132
-
raise ValueError(f"Value must be >= {cls.minimum}")
133
-
134
-
if cls.maximum is not None and v > cls.maximum:
135
-
raise ValueError(f"Value must be <= {cls.maximum}")
136
-
137
-
return v
138
-
139
-
140
-
# String Types (from string.py)
141
-
class StringModel(DataModel):
142
-
"""Model for AT Protocol string type."""
143
-
144
-
value: str
145
-
"""String value"""
146
-
147
-
format: str | None = None
148
-
"""String format restriction"""
149
-
150
-
maxLength: int | None = None
151
-
"""Maximum length in UTF-8 bytes"""
152
-
153
-
minLength: int | None = None
154
-
"""Minimum length in UTF-8 bytes"""
155
-
156
-
knownValues: list[str] | None = None
157
-
"""Suggested/common values"""
158
-
159
-
enum: list[str] | None = None
160
-
"""Closed set of allowed values"""
161
-
162
-
default: str | None = None
163
-
"""Default value if not provided"""
164
-
165
-
const: str | None = None
166
-
"""Fixed constant value if specified"""
167
-
168
-
def __init__(self, **data: Any) -> None:
169
-
"""
170
-
Initialize string model with validation.
171
-
172
-
Args:
173
-
**data: Input data containing string value
174
-
175
-
Raises:
176
-
ValueError: If value violates constraints
177
-
"""
178
-
super().__init__(**data)
179
-
if self.const is not None and self.value != self.const:
180
-
raise ValueError(f"String value must be {self.const}")
181
-
182
-
@field_validator("value", mode="before")
183
-
def validateString(cls, v: Any) -> str:
184
-
"""
185
-
Validate and convert input to string.
186
-
187
-
Args:
188
-
v: Value to validate
189
-
190
-
Returns:
191
-
Validated string value
192
-
193
-
Raises:
194
-
ValueError: If value violates constraints
195
-
"""
196
-
if not isinstance(v, str):
197
-
v = str(v)
198
-
199
-
if cls.minLength is not None and len(v.encode()) < cls.minLength:
200
-
raise ValueError(f"String must be at least {cls.minLength} bytes")
201
-
202
-
if cls.maxLength is not None and len(v.encode()) > cls.maxLength:
203
-
raise ValueError(f"String must be at most {cls.maxLength} bytes")
204
-
205
-
if cls.enum and v not in cls.enum:
206
-
raise ValueError(f"Value must be one of {cls.enum}")
207
-
208
-
if cls.format:
209
-
cls._validateFormat(v)
210
-
211
-
return v
212
-
213
-
@classmethod
214
-
def _validateFormat(cls, v: str) -> None:
215
-
"""Validate string format based on specified format type."""
216
-
if cls.format == "datetime":
217
-
cls._validateDatetime(v)
218
-
elif cls.format == "uri":
219
-
cls._validateUri(v)
220
-
elif cls.format == "did":
221
-
cls._validateDid(v)
222
-
elif cls.format == "handle":
223
-
cls._validateHandle(v)
224
-
elif cls.format == "at-identifier":
225
-
cls._validateAtIdentifier(v)
226
-
elif cls.format == "at-uri":
227
-
cls._validateAtUri(v)
228
-
elif cls.format == "cid":
229
-
cls._validateCid(v)
230
-
elif cls.format == "nsid":
231
-
cls._validateNsid(v)
232
-
elif cls.format == "tid":
233
-
cls._validateTid(v)
234
-
elif cls.format == "record-key":
235
-
cls._validateRecordKey(v)
236
-
elif cls.format == "language":
237
-
cls._validateLanguage(v)
238
-
239
-
@classmethod
240
-
def _validateDid(cls, v: str) -> None:
241
-
"""Validate DID format"""
242
-
if len(v) > 2048:
243
-
raise ValueError("DID too long")
244
-
if not re.match(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$", v):
245
-
raise ValueError("Invalid DID format")
246
-
247
-
@classmethod
248
-
def _validateHandle(cls, v: str) -> None:
249
-
"""Validate handle format"""
250
-
if not re.match(
251
-
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])?$",
252
-
v,
253
-
):
254
-
raise ValueError("Handle contains invalid characters")
255
-
if len(v) > 253:
256
-
raise ValueError("Handle too long")
257
-
258
-
@classmethod
259
-
def _validateAtIdentifier(cls, v: str) -> None:
260
-
"""Validate at-identifier format (DID or handle)"""
261
-
try:
262
-
if v.startswith("did:"):
263
-
cls._validateDid(v)
264
-
else:
265
-
cls._validateHandle(v)
266
-
except ValueError as e:
267
-
raise ValueError(f"Invalid at-identifier: {e}")
268
-
269
-
@classmethod
270
-
def _validateAtUri(cls, v: str) -> None:
271
-
"""Validate AT-URI format"""
272
-
if not v.startswith("at://"):
273
-
raise ValueError("AT-URI must start with 'at://'")
274
-
if len(v) > 8192:
275
-
raise ValueError("AT-URI too long")
276
-
if v.endswith("/"):
277
-
raise ValueError("AT-URI cannot have trailing slash")
278
-
279
-
parts = v[5:].split("/")
280
-
authority = parts[0]
281
-
282
-
if not authority:
283
-
raise ValueError("AT-URI must have authority")
284
-
285
-
if authority.startswith("did:"):
286
-
if len(authority) > 2048:
287
-
raise ValueError("DID too long")
288
-
if ":" not in authority[4:]:
289
-
raise ValueError("Invalid DID format")
290
-
else:
291
-
if not re.match(r"^[a-z0-9.-]+$", authority):
292
-
raise ValueError("Invalid handle characters")
293
-
if len(authority) > 253:
294
-
raise ValueError("Handle too long")
295
-
296
-
if len(parts) > 1:
297
-
if len(parts) > 3:
298
-
raise ValueError("AT-URI path too deep")
299
-
300
-
collection = parts[1]
301
-
if not re.match(r"^[a-zA-Z0-9.-]+$", collection):
302
-
raise ValueError("Invalid collection NSID")
303
-
304
-
if len(parts) > 2:
305
-
rkey = parts[2]
306
-
if not rkey:
307
-
raise ValueError("Record key cannot be empty")
308
-
if not re.match(r"^[a-zA-Z0-9._:%-~]+$", rkey):
309
-
raise ValueError("Invalid record key characters")
310
-
311
-
@classmethod
312
-
def _validateCid(cls, v: str) -> None:
313
-
"""Validate CID string format"""
314
-
if len(v) > 100:
315
-
raise ValueError("CID too long")
316
-
if not re.match(r"^[a-zA-Z]+$", v):
317
-
raise ValueError("CID contains invalid characters")
318
-
319
-
@classmethod
320
-
def _validateNsid(cls, v: str) -> None:
321
-
"""Validate NSID format"""
322
-
if len(v) > 317:
323
-
raise ValueError("NSID too long")
324
-
if not re.match(
325
-
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})?)$",
326
-
v,
327
-
):
328
-
raise ValueError("NSID contains invalid characters")
329
-
330
-
@classmethod
331
-
def _validateTid(cls, v: str) -> None:
332
-
"""Validate TID format"""
333
-
if len(v) > 13:
334
-
raise ValueError("TID too long")
335
-
if not re.match(
336
-
r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$", v
337
-
):
338
-
raise ValueError("TID contains invalid characters")
339
-
340
-
@classmethod
341
-
def _validateRecordKey(cls, v: str) -> None:
342
-
"""Validate record-key format"""
343
-
if len(v) > 512:
344
-
raise ValueError("Record key too long")
345
-
if v == "." or v == "..":
346
-
raise ValueError(f"Record key is {v}, which is not allowed")
347
-
if not re.match(r"^[a-zA-Z0-9._:%-~]+$", v):
348
-
raise ValueError("Record key contains invalid characters")
349
-
350
-
@classmethod
351
-
def _validateLanguage(cls, v: str) -> None:
352
-
"""Validate BCP 47 language tag"""
353
-
if not re.match(r"^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$", v):
354
-
raise ValueError("Invalid language tag format")
355
-
cls._validateDatetime(v)
356
-
elif cls.format == "uri":
357
-
cls._validateUri(v)
358
-
elif cls.format == "did":
359
-
cls._validateDid(v)
360
-
elif cls.format == "handle":
361
-
cls._validateHandle(v)
362
-
elif cls.format == "at-identifier":
363
-
cls._validateAtIdentifier(v)
364
-
elif cls.format == "at-uri":
365
-
cls._validateAtUri(v)
366
-
elif cls.format == "cid":
367
-
cls._validateCid(v)
368
-
elif cls.format == "nsid":
369
-
cls._validateNsid(v)
370
-
elif cls.format == "tid":
371
-
cls._validateTid(v)
372
-
elif cls.format == "record-key":
373
-
cls._validateRecordKey(v)
374
-
elif cls.format == "language":
375
-
cls._validateLanguage(v)
376
-
377
-
@classmethod
378
-
def _validateDatetime(cls, v: str) -> None:
379
-
"""Validate RFC 3339 datetime format"""
380
-
try:
381
-
datetime.fromisoformat(v.replace("Z", "+00:00"))
382
-
except ValueError:
383
-
raise ValueError("Invalid datetime format")
384
-
385
-
@classmethod
386
-
def _validateUri(cls, v: str) -> None:
387
-
"""Validate URI format"""
388
-
if len(v) > 8192:
389
-
raise ValueError("URI too long")
390
-
if not re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:.+", v):
391
-
raise ValueError("Invalid URI format")
392
-
393
-
# ... (other validation methods remain the same)
394
-
395
-
396
-
# Binary Types (from binary.py)
397
-
class BytesModel(DataModel):
398
-
"""Model for AT Protocol bytes type."""
399
-
400
-
value: bytes
401
-
"""Raw binary data"""
402
-
403
-
minLength: int | None = None
404
-
"""Minimum size in bytes"""
405
-
406
-
maxLength: int | None = None
407
-
"""Maximum size in bytes"""
408
-
409
-
def __init__(self, **data: Any) -> None:
410
-
"""
411
-
Initialize bytes model with validation.
412
-
413
-
Args:
414
-
**data: Input data containing bytes value and constraints
415
-
416
-
Raises:
417
-
ValidationError: If length constraints are violated
418
-
"""
419
-
super().__init__(**data)
420
-
421
-
@field_validator("value")
422
-
def validateLength(cls, v: bytes, info: Any) -> bytes:
423
-
"""
424
-
Validate bytes length against constraints.
425
-
426
-
Args:
427
-
v: Bytes value to validate
428
-
info: Validation info containing field values
429
-
430
-
Returns:
431
-
Validated bytes
432
-
433
-
Raises:
434
-
ValidationError: If length constraints are violated
435
-
"""
436
-
minLen = info.data.get("minLength")
437
-
maxLen = info.data.get("maxLength")
438
-
439
-
if minLen is not None and len(v) < minLen:
440
-
raise ValidationError(
441
-
field="value",
442
-
message=f"Bytes length {len(v)} is less than minimum {minLen}",
443
-
)
444
-
445
-
if maxLen is not None and len(v) > maxLen:
446
-
raise ValidationError(
447
-
field="value", message=f"Bytes length {len(v)} exceeds maximum {maxLen}"
448
-
)
449
-
450
-
return v
451
-
452
-
@field_serializer("value")
453
-
def serializeBytes(self, v: bytes) -> dict[str, str]:
454
-
"""
455
-
Serialize bytes to JSON format with base64 encoding.
456
-
457
-
Args:
458
-
v: Bytes to serialize
459
-
460
-
Returns:
461
-
Dictionary with base64 encoded bytes
462
-
463
-
Raises:
464
-
SerializationError: If encoding fails
465
-
"""
466
-
try:
467
-
return {"$bytes": base64.b64encode(v).decode()}
468
-
except Exception as e:
469
-
raise SerializationError("value", f"Failed to encode bytes: {e}")
470
-
471
-
472
-
class CidLinkModel(DataModel):
473
-
"""Model for AT Protocol CID link type."""
474
-
475
-
link: CIDv1
476
-
"""CID reference to linked content"""
477
-
478
-
def __init__(self, **data: Any) -> None:
479
-
"""
480
-
Initialize CID link model with validation.
481
-
482
-
Args:
483
-
**data: Input data containing CID link
484
-
485
-
Raises:
486
-
InvalidCIDError: If CID is invalid
487
-
"""
488
-
if isinstance(data.get("link"), str):
489
-
try:
490
-
data["link"] = make_cid(data["link"])
491
-
except ValueError as e:
492
-
raise InvalidCIDError(f"Invalid CID: {e}")
493
-
494
-
super().__init__(**data)
495
-
496
-
@field_serializer("link")
497
-
def serializeCid(self, v: CIDv1) -> dict[str, str]:
498
-
"""
499
-
Serialize CID to JSON format.
500
-
501
-
Args:
502
-
v: CID to serialize
503
-
504
-
Returns:
505
-
Dictionary with string CID representation
506
-
"""
507
-
return {"$link": str(v)}
508
-
509
-
510
-
# Complex Types (from complex.py)
511
-
class ArrayModel(DataModel):
512
-
"""Model for AT Protocol array type."""
513
-
514
-
items: Any
515
-
"""Schema definition for array elements"""
516
-
517
-
minLength: int | None = None
518
-
"""Minimum number of elements"""
519
-
520
-
maxLength: int | None = None
521
-
"""Maximum number of elements"""
522
-
523
-
value: list[Any]
524
-
"""Array values"""
525
-
526
-
def __init__(self, **data: Any) -> None:
527
-
"""
528
-
Initialize array model with validation.
529
-
530
-
Args:
531
-
**data: Input data containing array values
532
-
533
-
Raises:
534
-
ValueError: If array violates constraints
535
-
"""
536
-
super().__init__(**data)
537
-
538
-
@field_validator("value", mode="before")
539
-
def validateArray(cls, v: Any) -> list[Any]:
540
-
"""
541
-
Validate array structure and elements.
542
-
543
-
Args:
544
-
v: Value to validate
545
-
546
-
Returns:
547
-
Validated array
548
-
549
-
Raises:
550
-
ValueError: If array violates constraints
551
-
"""
552
-
if not isinstance(v, list):
553
-
raise ValueError("Value must be an array")
554
-
555
-
if cls.minLength is not None and len(v) < cls.minLength:
556
-
raise ValueError(f"Array must have at least {cls.minLength} items")
557
-
558
-
if cls.maxLength is not None and len(v) > cls.maxLength:
559
-
raise ValueError(f"Array must have at most {cls.maxLength} items")
560
-
561
-
return v
562
-
563
-
564
-
class ObjectModel(DataModel):
565
-
"""Model for AT Protocol object type."""
566
-
567
-
properties: dict[str, Any]
568
-
"""Map of property names to their schema definitions"""
569
-
570
-
required: list[str] | None = None
571
-
"""List of required property names"""
572
-
573
-
nullable: list[str] | None = None
574
-
"""List of properties that can be null"""
575
-
576
-
value: dict[str, Any]
577
-
"""Object property values"""
578
-
579
-
def __init__(self, **data: Any) -> None:
580
-
"""
581
-
Initialize object model with validation.
582
-
583
-
Args:
584
-
**data: Input data containing object properties
585
-
586
-
Raises:
587
-
ValueError: If object violates constraints
588
-
"""
589
-
super().__init__(**data)
590
-
591
-
@field_validator("value", mode="before")
592
-
def validateObject(cls, v: Any) -> dict[str, Any]:
593
-
"""
594
-
Validate object structure and properties.
595
-
596
-
Args:
597
-
v: Value to validate
598
-
599
-
Returns:
600
-
Validated object
601
-
602
-
Raises:
603
-
ValueError: If object violates constraints
604
-
"""
605
-
if not isinstance(v, dict):
606
-
raise ValueError("Value must be an object")
607
-
608
-
if cls.required:
609
-
for field in cls.required:
610
-
if field not in v:
611
-
raise ValueError(f"Missing required field: {field}")
612
-
613
-
if cls.nullable:
614
-
for field, value in v.items():
615
-
if field not in cls.nullable and value is None:
616
-
raise ValueError(f"Field {field} cannot be null")
617
-
618
-
return v
619
-
620
-
621
-
class ParamsModel(DataModel):
622
-
"""Model for AT Protocol params type."""
623
-
624
-
required: list[str] | None = None
625
-
"""List of required parameter names"""
626
-
627
-
properties: dict[str, Any]
628
-
"""Map of parameter names to their schema definitions"""
629
-
630
-
value: dict[str, Any]
631
-
"""Parameter values"""
632
-
633
-
def __init__(self, **data: Any) -> None:
634
-
"""
635
-
Initialize params model with validation.
636
-
637
-
Args:
638
-
**data: Input data containing parameter values
639
-
640
-
Raises:
641
-
ValueError: If parameters violate constraints
642
-
"""
643
-
super().__init__(**data)
644
-
645
-
@field_validator("value", mode="before")
646
-
def validateParams(cls, v: Any) -> dict[str, Any]:
647
-
"""
648
-
Validate parameters structure and values.
649
-
650
-
Args:
651
-
v: Value to validate
652
-
653
-
Returns:
654
-
Validated parameters dictionary
655
-
656
-
Raises:
657
-
ValueError: If parameters violate constraints
658
-
"""
659
-
if not isinstance(v, dict):
660
-
raise ValueError("Value must be a dictionary of parameters")
661
-
662
-
validated = dict(v)
663
-
664
-
if cls.required:
665
-
for param in cls.required:
666
-
if param not in validated:
667
-
raise ValueError(f"Missing required parameter: {param}")
668
-
669
-
for param, value in validated.items():
670
-
if param in cls.properties:
671
-
propType = cls.properties[param].get("type")
672
-
if propType == "boolean" and not isinstance(value, bool):
673
-
raise ValueError(f"Parameter {param} must be boolean")
674
-
elif propType == "integer" and not isinstance(value, int):
675
-
raise ValueError(f"Parameter {param} must be integer")
676
-
elif propType == "string" and not isinstance(value, str):
677
-
raise ValueError(f"Parameter {param} must be string")
678
-
elif propType == "array":
679
-
if not isinstance(value, list):
680
-
raise ValueError(f"Parameter {param} must be array")
681
-
if "items" in cls.properties[param]:
682
-
itemType = cls.properties[param]["items"].get("type")
683
-
for item in value:
684
-
if itemType == "boolean" and not isinstance(item, bool):
685
-
raise ValueError(
686
-
f"Array item in {param} must be boolean"
687
-
)
688
-
elif itemType == "integer" and not isinstance(item, int):
689
-
raise ValueError(
690
-
f"Array item in {param} must be integer"
691
-
)
692
-
elif itemType == "string" and not isinstance(item, str):
693
-
raise ValueError(
694
-
f"Array item in {param} must be string"
695
-
)
696
-
elif itemType == "unknown" and not isinstance(item, dict):
697
-
raise ValueError(
698
-
f"Array item in {param} must be object"
699
-
)
700
-
elif propType == "unknown" and not isinstance(value, dict):
701
-
raise ValueError(f"Parameter {param} must be object")
702
-
703
-
return validated
+132
src/atpasser/model/types/binary.py
+132
src/atpasser/model/types/binary.py
···
1
+
"""
2
+
Binary data types for AT Protocol Lexicon models.
3
+
4
+
Includes models for bytes, CID links and other binary data formats.
5
+
"""
6
+
from typing import Any
7
+
import base64
8
+
from pydantic import field_validator, field_serializer
9
+
from cid.cid import CIDv1, make_cid
10
+
from ..base import DataModel
11
+
from ..exceptions import ValidationError, SerializationError, InvalidCIDError
12
+
13
+
class BytesModel(DataModel):
14
+
"""
15
+
Model for AT Protocol bytes type.
16
+
17
+
Represents raw binary data that is encoded as base64 in JSON format.
18
+
"""
19
+
20
+
value: bytes
21
+
"""Raw binary data"""
22
+
23
+
min_length: int | None = None
24
+
"""Minimum size in bytes"""
25
+
26
+
max_length: int | None = None
27
+
"""Maximum size in bytes"""
28
+
29
+
def __init__(self, **data: Any) -> None:
30
+
"""
31
+
Initialize bytes model with validation.
32
+
33
+
Args:
34
+
**data: Input data containing bytes value and constraints
35
+
36
+
Raises:
37
+
ValidationError: If length constraints are violated
38
+
"""
39
+
super().__init__(**data)
40
+
41
+
@field_validator("value")
42
+
def validate_length(cls, v: bytes, info: Any) -> bytes:
43
+
"""
44
+
Validate bytes length against constraints.
45
+
46
+
Args:
47
+
v: Bytes value to validate
48
+
info: Validation info containing field values
49
+
50
+
Returns:
51
+
Validated bytes
52
+
53
+
Raises:
54
+
ValidationError: If length constraints are violated
55
+
"""
56
+
min_len = info.data.get("min_length")
57
+
max_len = info.data.get("max_length")
58
+
59
+
if min_len is not None and len(v) < min_len:
60
+
raise ValidationError(
61
+
field="value",
62
+
message=f"Bytes length {len(v)} is less than minimum {min_len}"
63
+
)
64
+
65
+
if max_len is not None and len(v) > max_len:
66
+
raise ValidationError(
67
+
field="value",
68
+
message=f"Bytes length {len(v)} exceeds maximum {max_len}"
69
+
)
70
+
71
+
return v
72
+
73
+
@field_serializer("value")
74
+
def serialize_bytes(self, v: bytes) -> dict[str, str]:
75
+
"""
76
+
Serialize bytes to JSON format with base64 encoding.
77
+
78
+
Args:
79
+
v: Bytes to serialize
80
+
81
+
Returns:
82
+
Dictionary with base64 encoded bytes
83
+
84
+
Raises:
85
+
SerializationError: If encoding fails
86
+
"""
87
+
try:
88
+
return {"$bytes": base64.b64encode(v).decode()}
89
+
except Exception as e:
90
+
raise SerializationError("value", f"Failed to encode bytes: {e}")
91
+
92
+
class CidLinkModel(DataModel):
93
+
"""
94
+
Model for AT Protocol CID link type.
95
+
96
+
Represents content-addressable links using CIDs (Content Identifiers).
97
+
"""
98
+
99
+
link: CIDv1
100
+
"""CID reference to linked content"""
101
+
102
+
def __init__(self, **data: Any) -> None:
103
+
"""
104
+
Initialize CID link model with validation.
105
+
106
+
Args:
107
+
**data: Input data containing CID link
108
+
109
+
Raises:
110
+
InvalidCIDError: If CID is invalid
111
+
"""
112
+
# Handle JSON format with $link field
113
+
if isinstance(data.get("link"), str):
114
+
try:
115
+
data["link"] = make_cid(data["link"])
116
+
except ValueError as e:
117
+
raise InvalidCIDError(f"Invalid CID: {e}")
118
+
119
+
super().__init__(**data)
120
+
121
+
@field_serializer("link")
122
+
def serialize_cid(self, v: CIDv1) -> dict[str, str]:
123
+
"""
124
+
Serialize CID to JSON format.
125
+
126
+
Args:
127
+
v: CID to serialize
128
+
129
+
Returns:
130
+
Dictionary with string CID representation
131
+
"""
132
+
return {"$link": str(v)}
+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")