···
2
+
AT Protocol Lexicon Type Models
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)
12
+
from typing import Any
15
+
from datetime import datetime
16
+
from pydantic import field_validator, field_serializer
17
+
from cid.cid import CIDv1, make_cid
19
+
from .base import DataModel
20
+
from .exceptions import ValidationError, SerializationError, InvalidCIDError
23
+
class BooleanModel(DataModel):
24
+
"""Model for AT Protocol boolean type."""
29
+
default: bool | None = None
30
+
"""Default value if not provided"""
32
+
const: bool | None = None
33
+
"""Fixed constant value if specified"""
35
+
def __init__(self, **data: Any) -> None:
37
+
Initialize boolean model with validation.
40
+
**data: Input data containing boolean value
43
+
ValueError: If value doesn't match const or is not boolean
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}")
49
+
@field_validator("value", mode="before")
50
+
def validateBoolean(cls, v: Any) -> bool:
52
+
Validate and convert input to boolean.
55
+
v: Value to validate
58
+
Validated boolean value
61
+
ValueError: If value cannot be converted to boolean
63
+
if isinstance(v, bool):
65
+
if isinstance(v, str):
66
+
if v.lower() in ("true", "1"):
68
+
if v.lower() in ("false", "0"):
70
+
raise ValueError("Value must be a boolean")
73
+
class IntegerModel(DataModel):
74
+
"""Model for AT Protocol integer type."""
79
+
minimum: int | None = None
80
+
"""Minimum acceptable value"""
82
+
maximum: int | None = None
83
+
"""Maximum acceptable value"""
85
+
enum: list[int] | None = None
86
+
"""Closed set of allowed values"""
88
+
default: int | None = None
89
+
"""Default value if not provided"""
91
+
const: int | None = None
92
+
"""Fixed constant value if specified"""
94
+
def __init__(self, **data: Any) -> None:
96
+
Initialize integer model with validation.
99
+
**data: Input data containing integer value
102
+
ValueError: If value violates constraints
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}")
108
+
@field_validator("value", mode="before")
109
+
def validateInteger(cls, v: Any) -> int:
111
+
Validate and convert input to integer.
114
+
v: Value to validate
117
+
Validated integer value
120
+
ValueError: If value violates constraints
122
+
if not isinstance(v, int):
125
+
except (TypeError, ValueError):
126
+
raise ValueError("Value must be an integer")
128
+
if cls.enum and v not in cls.enum:
129
+
raise ValueError(f"Value must be one of {cls.enum}")
131
+
if cls.minimum is not None and v < cls.minimum:
132
+
raise ValueError(f"Value must be >= {cls.minimum}")
134
+
if cls.maximum is not None and v > cls.maximum:
135
+
raise ValueError(f"Value must be <= {cls.maximum}")
140
+
# String Types (from string.py)
141
+
class StringModel(DataModel):
142
+
"""Model for AT Protocol string type."""
147
+
format: str | None = None
148
+
"""String format restriction"""
150
+
maxLength: int | None = None
151
+
"""Maximum length in UTF-8 bytes"""
153
+
minLength: int | None = None
154
+
"""Minimum length in UTF-8 bytes"""
156
+
knownValues: list[str] | None = None
157
+
"""Suggested/common values"""
159
+
enum: list[str] | None = None
160
+
"""Closed set of allowed values"""
162
+
default: str | None = None
163
+
"""Default value if not provided"""
165
+
const: str | None = None
166
+
"""Fixed constant value if specified"""
168
+
def __init__(self, **data: Any) -> None:
170
+
Initialize string model with validation.
173
+
**data: Input data containing string value
176
+
ValueError: If value violates constraints
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}")
182
+
@field_validator("value", mode="before")
183
+
def validateString(cls, v: Any) -> str:
185
+
Validate and convert input to string.
188
+
v: Value to validate
191
+
Validated string value
194
+
ValueError: If value violates constraints
196
+
if not isinstance(v, str):
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")
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")
205
+
if cls.enum and v not in cls.enum:
206
+
raise ValueError(f"Value must be one of {cls.enum}")
209
+
cls._validateFormat(v)
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)
240
+
def _validateDid(cls, v: str) -> None:
241
+
"""Validate DID format"""
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")
248
+
def _validateHandle(cls, v: str) -> None:
249
+
"""Validate handle format"""
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])?$",
254
+
raise ValueError("Handle contains invalid characters")
256
+
raise ValueError("Handle too long")
259
+
def _validateAtIdentifier(cls, v: str) -> None:
260
+
"""Validate at-identifier format (DID or handle)"""
262
+
if v.startswith("did:"):
263
+
cls._validateDid(v)
265
+
cls._validateHandle(v)
266
+
except ValueError as e:
267
+
raise ValueError(f"Invalid at-identifier: {e}")
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://'")
275
+
raise ValueError("AT-URI too long")
276
+
if v.endswith("/"):
277
+
raise ValueError("AT-URI cannot have trailing slash")
279
+
parts = v[5:].split("/")
280
+
authority = parts[0]
283
+
raise ValueError("AT-URI must have authority")
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")
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")
298
+
raise ValueError("AT-URI path too deep")
300
+
collection = parts[1]
301
+
if not re.match(r"^[a-zA-Z0-9.-]+$", collection):
302
+
raise ValueError("Invalid collection NSID")
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")
312
+
def _validateCid(cls, v: str) -> None:
313
+
"""Validate CID string format"""
315
+
raise ValueError("CID too long")
316
+
if not re.match(r"^[a-zA-Z]+$", v):
317
+
raise ValueError("CID contains invalid characters")
320
+
def _validateNsid(cls, v: str) -> None:
321
+
"""Validate NSID format"""
323
+
raise ValueError("NSID too long")
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})?)$",
328
+
raise ValueError("NSID contains invalid characters")
331
+
def _validateTid(cls, v: str) -> None:
332
+
"""Validate TID format"""
334
+
raise ValueError("TID too long")
336
+
r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$", v
338
+
raise ValueError("TID contains invalid characters")
341
+
def _validateRecordKey(cls, v: str) -> None:
342
+
"""Validate record-key format"""
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")
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)
378
+
def _validateDatetime(cls, v: str) -> None:
379
+
"""Validate RFC 3339 datetime format"""
381
+
datetime.fromisoformat(v.replace("Z", "+00:00"))
383
+
raise ValueError("Invalid datetime format")
386
+
def _validateUri(cls, v: str) -> None:
387
+
"""Validate URI format"""
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")
393
+
# ... (other validation methods remain the same)
396
+
# Binary Types (from binary.py)
397
+
class BytesModel(DataModel):
398
+
"""Model for AT Protocol bytes type."""
401
+
"""Raw binary data"""
403
+
minLength: int | None = None
404
+
"""Minimum size in bytes"""
406
+
maxLength: int | None = None
407
+
"""Maximum size in bytes"""
409
+
def __init__(self, **data: Any) -> None:
411
+
Initialize bytes model with validation.
414
+
**data: Input data containing bytes value and constraints
417
+
ValidationError: If length constraints are violated
419
+
super().__init__(**data)
421
+
@field_validator("value")
422
+
def validateLength(cls, v: bytes, info: Any) -> bytes:
424
+
Validate bytes length against constraints.
427
+
v: Bytes value to validate
428
+
info: Validation info containing field values
434
+
ValidationError: If length constraints are violated
436
+
minLen = info.data.get("minLength")
437
+
maxLen = info.data.get("maxLength")
439
+
if minLen is not None and len(v) < minLen:
440
+
raise ValidationError(
442
+
message=f"Bytes length {len(v)} is less than minimum {minLen}",
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}"
452
+
@field_serializer("value")
453
+
def serializeBytes(self, v: bytes) -> dict[str, str]:
455
+
Serialize bytes to JSON format with base64 encoding.
458
+
v: Bytes to serialize
461
+
Dictionary with base64 encoded bytes
464
+
SerializationError: If encoding fails
467
+
return {"$bytes": base64.b64encode(v).decode()}
468
+
except Exception as e:
469
+
raise SerializationError("value", f"Failed to encode bytes: {e}")
472
+
class CidLinkModel(DataModel):
473
+
"""Model for AT Protocol CID link type."""
476
+
"""CID reference to linked content"""
478
+
def __init__(self, **data: Any) -> None:
480
+
Initialize CID link model with validation.
483
+
**data: Input data containing CID link
486
+
InvalidCIDError: If CID is invalid
488
+
if isinstance(data.get("link"), str):
490
+
data["link"] = make_cid(data["link"])
491
+
except ValueError as e:
492
+
raise InvalidCIDError(f"Invalid CID: {e}")
494
+
super().__init__(**data)
496
+
@field_serializer("link")
497
+
def serializeCid(self, v: CIDv1) -> dict[str, str]:
499
+
Serialize CID to JSON format.
502
+
v: CID to serialize
505
+
Dictionary with string CID representation
507
+
return {"$link": str(v)}
510
+
# Complex Types (from complex.py)
511
+
class ArrayModel(DataModel):
512
+
"""Model for AT Protocol array type."""
515
+
"""Schema definition for array elements"""
517
+
minLength: int | None = None
518
+
"""Minimum number of elements"""
520
+
maxLength: int | None = None
521
+
"""Maximum number of elements"""
526
+
def __init__(self, **data: Any) -> None:
528
+
Initialize array model with validation.
531
+
**data: Input data containing array values
534
+
ValueError: If array violates constraints
536
+
super().__init__(**data)
538
+
@field_validator("value", mode="before")
539
+
def validateArray(cls, v: Any) -> list[Any]:
541
+
Validate array structure and elements.
544
+
v: Value to validate
550
+
ValueError: If array violates constraints
552
+
if not isinstance(v, list):
553
+
raise ValueError("Value must be an array")
555
+
if cls.minLength is not None and len(v) < cls.minLength:
556
+
raise ValueError(f"Array must have at least {cls.minLength} items")
558
+
if cls.maxLength is not None and len(v) > cls.maxLength:
559
+
raise ValueError(f"Array must have at most {cls.maxLength} items")
564
+
class ObjectModel(DataModel):
565
+
"""Model for AT Protocol object type."""
567
+
properties: dict[str, Any]
568
+
"""Map of property names to their schema definitions"""
570
+
required: list[str] | None = None
571
+
"""List of required property names"""
573
+
nullable: list[str] | None = None
574
+
"""List of properties that can be null"""
576
+
value: dict[str, Any]
577
+
"""Object property values"""
579
+
def __init__(self, **data: Any) -> None:
581
+
Initialize object model with validation.
584
+
**data: Input data containing object properties
587
+
ValueError: If object violates constraints
589
+
super().__init__(**data)
591
+
@field_validator("value", mode="before")
592
+
def validateObject(cls, v: Any) -> dict[str, Any]:
594
+
Validate object structure and properties.
597
+
v: Value to validate
603
+
ValueError: If object violates constraints
605
+
if not isinstance(v, dict):
606
+
raise ValueError("Value must be an object")
609
+
for field in cls.required:
611
+
raise ValueError(f"Missing required field: {field}")
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")
621
+
class ParamsModel(DataModel):
622
+
"""Model for AT Protocol params type."""
624
+
required: list[str] | None = None
625
+
"""List of required parameter names"""
627
+
properties: dict[str, Any]
628
+
"""Map of parameter names to their schema definitions"""
630
+
value: dict[str, Any]
631
+
"""Parameter values"""
633
+
def __init__(self, **data: Any) -> None:
635
+
Initialize params model with validation.
638
+
**data: Input data containing parameter values
641
+
ValueError: If parameters violate constraints
643
+
super().__init__(**data)
645
+
@field_validator("value", mode="before")
646
+
def validateParams(cls, v: Any) -> dict[str, Any]:
648
+
Validate parameters structure and values.
651
+
v: Value to validate
654
+
Validated parameters dictionary
657
+
ValueError: If parameters violate constraints
659
+
if not isinstance(v, dict):
660
+
raise ValueError("Value must be a dictionary of parameters")
662
+
validated = dict(v)
665
+
for param in cls.required:
666
+
if param not in validated:
667
+
raise ValueError(f"Missing required parameter: {param}")
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")
684
+
if itemType == "boolean" and not isinstance(item, bool):
686
+
f"Array item in {param} must be boolean"
688
+
elif itemType == "integer" and not isinstance(item, int):
690
+
f"Array item in {param} must be integer"
692
+
elif itemType == "string" and not isinstance(item, str):
694
+
f"Array item in {param} must be string"
696
+
elif itemType == "unknown" and not isinstance(item, dict):
698
+
f"Array item in {param} must be object"
700
+
elif propType == "unknown" and not isinstance(value, dict):
701
+
raise ValueError(f"Parameter {param} must be object")