Revert "combine into one file"

This reverts commit ad283513ab833fe3659e959ddd1ac5a92e576f89.

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