+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")