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