combine into one file

Changed files
+703 -1221
src
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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")