rebuild data model

+91 -1
poetry.lock
··· 285 285 ] 286 286 287 287 [[package]] 288 + name = "colorama" 289 + version = "0.4.6" 290 + description = "Cross-platform colored terminal text." 291 + optional = false 292 + python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 293 + groups = ["main"] 294 + markers = "sys_platform == \"win32\"" 295 + files = [ 296 + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 297 + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 298 + ] 299 + 300 + [[package]] 288 301 name = "cryptography" 289 302 version = "45.0.7" 290 303 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." ··· 428 441 429 442 [package.extras] 430 443 all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 444 + 445 + [[package]] 446 + name = "iniconfig" 447 + version = "2.1.0" 448 + description = "brain-dead simple config-ini parsing" 449 + optional = false 450 + python-versions = ">=3.8" 451 + groups = ["main"] 452 + files = [ 453 + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 454 + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 455 + ] 431 456 432 457 [[package]] 433 458 name = "jsonpath-ng" ··· 705 730 ] 706 731 707 732 [[package]] 733 + name = "packaging" 734 + version = "25.0" 735 + description = "Core utilities for Python packages" 736 + optional = false 737 + python-versions = ">=3.8" 738 + groups = ["main"] 739 + files = [ 740 + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 741 + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 742 + ] 743 + 744 + [[package]] 745 + name = "pluggy" 746 + version = "1.6.0" 747 + description = "plugin and hook calling mechanisms for python" 748 + optional = false 749 + python-versions = ">=3.9" 750 + groups = ["main"] 751 + files = [ 752 + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 753 + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 754 + ] 755 + 756 + [package.extras] 757 + dev = ["pre-commit", "tox"] 758 + testing = ["coverage", "pytest", "pytest-benchmark"] 759 + 760 + [[package]] 708 761 name = "ply" 709 762 version = "3.11" 710 763 description = "Python Lex & Yacc" ··· 787 840 ] 788 841 789 842 [[package]] 843 + name = "pygments" 844 + version = "2.19.2" 845 + description = "Pygments is a syntax highlighting package written in Python." 846 + optional = false 847 + python-versions = ">=3.8" 848 + groups = ["main"] 849 + files = [ 850 + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 851 + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 852 + ] 853 + 854 + [package.extras] 855 + windows-terminal = ["colorama (>=0.4.6)"] 856 + 857 + [[package]] 790 858 name = "pyld" 791 859 version = "2.0.4" 792 860 description = "Python implementation of the JSON-LD API" ··· 827 895 sha3 = ["pysha3"] 828 896 829 897 [[package]] 898 + name = "pytest" 899 + version = "8.4.2" 900 + description = "pytest: simple powerful testing with Python" 901 + optional = false 902 + python-versions = ">=3.9" 903 + groups = ["main"] 904 + files = [ 905 + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, 906 + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, 907 + ] 908 + 909 + [package.dependencies] 910 + colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 911 + iniconfig = ">=1" 912 + packaging = ">=20" 913 + pluggy = ">=1.5,<2" 914 + pygments = ">=2.7.2" 915 + 916 + [package.extras] 917 + dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 918 + 919 + [[package]] 830 920 name = "python-baseconv" 831 921 version = "1.2.2" 832 922 description = "Convert numbers from base 10 integers to base X strings and back again." ··· 903 993 [metadata] 904 994 lock-version = "2.1" 905 995 python-versions = ">=3.13" 906 - content-hash = "5f4e5fd166bf6b2010ec5acaf545a0cfe376dcc5437530f3add4b58f10ce439f" 996 + content-hash = "959bc6b01857f65b67d47c12c46abc7af95aecf9913baf195392232500f2253a"
+1
pyproject.toml
··· 14 14 "jsonpath-ng (>=1.7.0,<2.0.0)", # for URI fragment support 15 15 "cryptography (>=45.0.7,<46.0.0)", # just keep it 16 16 "langcodes (>=3.5.0,<4.0.0)", # language codes support 17 + "pytest (>=8.4.2,<9.0.0)", 17 18 ] 18 19 license = "MIT OR Apache-2.0" 19 20 license-files = ["LICEN[CS]E.*"]
+49 -1
src/atpasser/data/__init__.py
··· 1 - from ._wrapper import * 1 + """ 2 + JSON module wrapper for ATProto data model. 3 + 4 + This module provides a JSON encoder and decoder that handle ATProto-specific 5 + data types, including bytes, CID links, and typed objects. 6 + """ 7 + 8 + from .encoder import JsonEncoder 9 + from .decoder import JsonDecoder, TypedObject 10 + from .hooks import TypeHookRegistry, type_handler 11 + from .types import ( 12 + TypeProcessor, 13 + TypeProcessorRegistry, 14 + register_type, 15 + register_type_encoder, 16 + register_type_class, 17 + unregister_type, 18 + get_type_decoder, 19 + get_type_encoder, 20 + has_type_processor, 21 + clear_type_processors, 22 + get_registered_types, 23 + create_processor_registry, 24 + ) 25 + from .wrapper import dump, dumps, load, loads 26 + 27 + __all__ = [ 28 + "JsonEncoder", 29 + "JsonDecoder", 30 + "TypedObject", 31 + "TypeHookRegistry", 32 + "type_handler", 33 + "TypeProcessor", 34 + "TypeProcessorRegistry", 35 + "register_type", 36 + "register_type_encoder", 37 + "register_type_class", 38 + "unregister_type", 39 + "get_type_decoder", 40 + "get_type_encoder", 41 + "has_type_processor", 42 + "clear_type_processors", 43 + "get_registered_types", 44 + "create_processor_registry", 45 + "dump", 46 + "dumps", 47 + "load", 48 + "loads", 49 + ]
-76
src/atpasser/data/_data.py
··· 1 - import base64 2 - from cid import CIDv0, CIDv1, cid, make_cid 3 - import json 4 - 5 - 6 - class Data: 7 - """ 8 - A class representing data with "$type" key. 9 - 10 - Attributes: 11 - type (str): The type of the data. 12 - json (str): Original object in JSON format. 13 - """ 14 - 15 - def __init__(self, dataType: str, json: str = "{}") -> None: 16 - """ 17 - Initalizes data object. 18 - 19 - Parameters: 20 - type (str): The type of the data. 21 - json (str): Original object in JSON format. 22 - """ 23 - self.type = dataType 24 - self.json = json 25 - 26 - def data(self): 27 - """ 28 - Loads data as a Python-friendly format. 29 - 30 - Returns: 31 - dict: Converted data from JSON object. 32 - """ 33 - return json.loads(self.json, object_hook=dataHook) 34 - 35 - 36 - def dataHook(data: dict): 37 - """ 38 - Treated as `JSONDecoder`'s `object_hook` 39 - 40 - Parameters: 41 - data: data in format that `JSONDecoder` like ;) 42 - """ 43 - if "$bytes" in data: 44 - return base64.b64decode(data["$bytes"]) 45 - elif "$link" in data: 46 - return make_cid(data["$link"]) 47 - elif "$type" in data: 48 - dataType = data["$type"] 49 - del data["$type"] 50 - return Data(dataType, json.dumps(data)) 51 - else: 52 - return data 53 - 54 - 55 - def _convertDataToFakeJSON(data): 56 - if isinstance(data, bytes): 57 - return {"$bytes": base64.b64encode(data)} 58 - elif isinstance(data, (CIDv0, CIDv1)): 59 - return {"link": data.encode()} 60 - elif isinstance(data, dict): 61 - for item in data: 62 - data[item] = _convertDataToFakeJSON(data[item]) 63 - elif isinstance(data, (tuple, list, set)): 64 - return [_convertDataToFakeJSON(item) for item in data] 65 - else: 66 - return data 67 - 68 - 69 - class DataEncoder(json.JSONEncoder): 70 - """ 71 - A superset of `JSONEncoder` to support ATProto data. 72 - """ 73 - 74 - def default(self, o): 75 - result = _convertDataToFakeJSON(o) 76 - return super().default(result)
-61
src/atpasser/data/_wrapper.py
··· 1 - from json import loads 2 - from typing import Callable, Any 3 - from ._data import * 4 - import functools 5 - 6 - # Pyright did the whole job. Thank it. 7 - 8 - 9 - class DataDecoder(json.JSONDecoder): 10 - """ 11 - A superset of `JSONDecoder` to support ATProto data. 12 - """ 13 - 14 - def __init__( 15 - self, 16 - *, 17 - object_hook: Callable[[dict[str, Any]], Any] | None = dataHook, 18 - parse_float: Callable[[str], Any] | None = None, 19 - parse_int: Callable[[str], Any] | None = None, 20 - parse_constant: Callable[[str], Any] | None = None, 21 - strict: bool = True, 22 - object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None, 23 - ) -> None: 24 - super().__init__( 25 - object_hook=object_hook, 26 - parse_float=parse_float, 27 - parse_int=parse_int, 28 - parse_constant=parse_constant, 29 - strict=strict, 30 - object_pairs_hook=object_pairs_hook, 31 - ) 32 - 33 - 34 - # Screw it. I have to make 4 `json`-like functions. 35 - 36 - 37 - def _dataDecoratorForDump(func): 38 - @functools.wraps(func) 39 - def wrapper(obj, *args, **kwargs): 40 - kwargs.setdefault("cls", DataEncoder) 41 - return func(obj, *args, **kwargs) 42 - 43 - return wrapper 44 - 45 - 46 - def _dataDecoratorForLoad(func): 47 - @functools.wraps(func) 48 - def wrapper(obj, *args, **kwargs): 49 - kwargs.setdefault("cls", DataDecoder) 50 - return func(obj, *args, **kwargs) 51 - 52 - return wrapper 53 - 54 - 55 - dump = _dataDecoratorForDump(json.dump) 56 - dumps = _dataDecoratorForDump(json.dumps) 57 - load = _dataDecoratorForLoad(json.load) 58 - loads = _dataDecoratorForLoad(json.loads) 59 - """ 60 - Wrapper of the JSON functions to support ATProto data. 61 - """
-137
src/atpasser/data/cbor.py
··· 1 - from datetime import tzinfo 2 - import typing 3 - import cbor2 4 - import cid 5 - 6 - from .data import dataHook, Data 7 - 8 - 9 - def tagHook(decoder: cbor2.CBORDecoder, tag: cbor2.CBORTag, shareable_index=None): 10 - """ 11 - A simple tag hook for CID support. 12 - """ 13 - return cid.from_bytes(tag.value) if tag.tag == 42 else tag 14 - 15 - 16 - class CBOREncoder(cbor2.CBOREncoder): 17 - """ 18 - Wrapper of cbor2.CBOREncoder. 19 - """ 20 - 21 - def __init__( 22 - self, 23 - fp: typing.IO[bytes], 24 - datetime_as_timestamp: bool = False, 25 - timezone: tzinfo | None = None, 26 - value_sharing: bool = False, 27 - default: ( 28 - typing.Callable[[cbor2.CBOREncoder, typing.Any], typing.Any] | None 29 - ) = None, 30 - canonical: bool = False, 31 - date_as_datetime: bool = False, 32 - string_referencing: bool = False, 33 - indefinite_containers: bool = False, 34 - ): 35 - super().__init__( 36 - fp, 37 - datetime_as_timestamp, 38 - timezone, 39 - value_sharing, 40 - default, 41 - canonical, 42 - date_as_datetime, 43 - string_referencing, 44 - indefinite_containers, 45 - ) 46 - 47 - @cbor2.shareable_encoder 48 - def cidOrDataEncoder(self: cbor2.CBOREncoder, value: cid.CIDv0 | cid.CIDv1 | Data): 49 - """ 50 - Encode CID or Data to CBOR Tag. 51 - """ 52 - if isinstance(value, (cid.CIDv0, cid.CIDv1)): 53 - self.encode(cbor2.CBORTag(42, value.encode())) 54 - elif isinstance(value, Data): 55 - self.encode(value.data()) 56 - 57 - 58 - def _cborObjectHook(decoder: cbor2.CBORDecoder, value): 59 - return dataHook(value) 60 - 61 - 62 - class CBORDecoder(cbor2.CBORDecoder): 63 - """ 64 - Wrapper of cbor2.CBORDecoder. 65 - """ 66 - 67 - def __init__( 68 - self, 69 - fp: typing.IO[bytes], 70 - tag_hook: ( 71 - typing.Callable[[cbor2.CBORDecoder, cbor2.CBORTag], typing.Any] | None 72 - ) = tagHook, 73 - object_hook: ( 74 - typing.Callable[ 75 - [cbor2.CBORDecoder, dict[typing.Any, typing.Any]], typing.Any 76 - ] 77 - | None 78 - ) = _cborObjectHook, 79 - str_errors: typing.Literal["strict", "error", "replace"] = "strict", 80 - ): 81 - super().__init__(fp, tag_hook, object_hook, str_errors) 82 - 83 - 84 - # Make things for CBOR again. 85 - 86 - from io import BytesIO 87 - 88 - 89 - def dumps( 90 - obj: object, 91 - datetime_as_timestamp: bool = False, 92 - timezone: tzinfo | None = None, 93 - value_sharing: bool = False, 94 - default: typing.Callable[[cbor2.CBOREncoder, typing.Any], typing.Any] | None = None, 95 - canonical: bool = False, 96 - date_as_datetime: bool = False, 97 - string_referencing: bool = False, 98 - indefinite_containers: bool = False, 99 - ) -> bytes: 100 - with BytesIO() as fp: 101 - CBOREncoder( 102 - fp, 103 - datetime_as_timestamp=datetime_as_timestamp, 104 - timezone=timezone, 105 - value_sharing=value_sharing, 106 - default=default, 107 - canonical=canonical, 108 - date_as_datetime=date_as_datetime, 109 - string_referencing=string_referencing, 110 - indefinite_containers=indefinite_containers, 111 - ).encode(obj) 112 - return fp.getvalue() 113 - 114 - 115 - def dump( 116 - obj: object, 117 - fp: typing.IO[bytes], 118 - datetime_as_timestamp: bool = False, 119 - timezone: tzinfo | None = None, 120 - value_sharing: bool = False, 121 - default: typing.Callable[[cbor2.CBOREncoder, typing.Any], typing.Any] | None = None, 122 - canonical: bool = False, 123 - date_as_datetime: bool = False, 124 - string_referencing: bool = False, 125 - indefinite_containers: bool = False, 126 - ) -> None: 127 - CBOREncoder( 128 - fp, 129 - datetime_as_timestamp=datetime_as_timestamp, 130 - timezone=timezone, 131 - value_sharing=value_sharing, 132 - default=default, 133 - canonical=canonical, 134 - date_as_datetime=date_as_datetime, 135 - string_referencing=string_referencing, 136 - indefinite_containers=indefinite_containers, 137 - ).encode(obj)
+178
src/atpasser/data/decoder.py
··· 1 + """ 2 + JSON decoder for ATProto data model. 3 + 4 + This module provides a JSON decoder that handles ATProto-specific data types, 5 + including bytes, CID links, and typed objects. 6 + """ 7 + 8 + import base64 9 + import json 10 + from typing import Any, Callable, Dict, Optional 11 + from cid import CIDv0, CIDv1, make_cid 12 + 13 + 14 + class JsonDecoder(json.JSONDecoder): 15 + """A JSON decoder that supports ATProto data types. 16 + 17 + This decoder extends the standard JSON decoder to handle ATProto-specific 18 + data types, including bytes, CID links, and typed objects. 19 + 20 + Attributes: 21 + type_hook_registry: Registry for type-specific hooks. 22 + encoding: The encoding to use for string deserialization. 23 + """ 24 + 25 + def __init__( 26 + self, 27 + *, 28 + object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None, 29 + type_hook_registry: Optional[Any] = None, 30 + type_processor_registry: Optional[Any] = None, 31 + encoding: str = "utf-8", 32 + **kwargs: Any 33 + ) -> None: 34 + """Initialize the JSON decoder. 35 + 36 + Args: 37 + object_hook: Optional function to call with each decoded object. 38 + type_hook_registry: Registry for type-specific hooks. 39 + type_processor_registry: Registry for type-specific processors. 40 + encoding: The encoding to use for string deserialization. 41 + **kwargs: Additional keyword arguments to pass to the parent class. 42 + """ 43 + # Use the type processor registry if provided, otherwise use the type hook registry 44 + if type_processor_registry is not None: 45 + type_hook_registry = type_processor_registry.to_hook_registry() 46 + elif type_hook_registry is None: 47 + from .hooks import get_global_registry 48 + type_hook_registry = get_global_registry() 49 + 50 + # Create a combined object hook that calls both the custom hook and our hook 51 + combined_hook = self._create_combined_hook(object_hook, type_hook_registry) 52 + 53 + super().__init__(object_hook=combined_hook, **kwargs) 54 + self.type_hook_registry = type_hook_registry 55 + self.type_processor_registry = type_processor_registry 56 + self.encoding = encoding 57 + 58 + def _create_combined_hook( 59 + self, 60 + custom_hook: Optional[Callable[[Dict[str, Any]], Any]], 61 + type_hook_registry: Optional[Any] 62 + ) -> Callable[[Dict[str, Any]], Any]: 63 + """Create a combined object hook function. 64 + 65 + Args: 66 + custom_hook: Optional custom object hook function. 67 + type_hook_registry: Registry for type-specific hooks. 68 + 69 + Returns: 70 + A combined object hook function. 71 + """ 72 + def combined_hook(obj: Dict[str, Any]) -> Any: 73 + # First, apply our ATProto-specific decoding 74 + decoded_obj = self._atproto_object_hook(obj) 75 + 76 + # Then, apply the custom hook if provided 77 + if custom_hook is not None: 78 + decoded_obj = custom_hook(decoded_obj) 79 + 80 + return decoded_obj 81 + 82 + return combined_hook 83 + 84 + def _atproto_object_hook(self, obj: Dict[str, Any]) -> Any: 85 + """Handle ATProto-specific object decoding. 86 + 87 + Args: 88 + obj: The object to decode. 89 + 90 + Returns: 91 + The decoded object. 92 + """ 93 + # Handle $bytes key (RFC-4648 base64 decoding) 94 + if "$bytes" in obj: 95 + if len(obj) != 1: 96 + # If there are other keys, this is invalid 97 + raise ValueError(f"Invalid $bytes object: {obj}") 98 + return base64.b64decode(obj["$bytes"].encode(self.encoding)) 99 + 100 + # Handle $link key (CID parsing) 101 + elif "$link" in obj: 102 + if len(obj) != 1: 103 + # If there are other keys, this is invalid 104 + raise ValueError(f"Invalid $link object: {obj}") 105 + return make_cid(obj["$link"]) 106 + 107 + # Handle $type key (typed objects) 108 + elif "$type" in obj: 109 + type_value = obj["$type"] 110 + remaining_obj = {k: v for k, v in obj.items() if k != "$type"} 111 + 112 + # Check if there's a registered type handler 113 + if self.type_hook_registry is not None: 114 + handler = self.type_hook_registry.get_handler(type_value) 115 + if handler is not None: 116 + return handler(remaining_obj) 117 + 118 + # If no handler is registered, return a typed object 119 + return TypedObject(type_value, remaining_obj) 120 + 121 + # Handle nested objects recursively 122 + elif isinstance(obj, dict): 123 + return {k: self._atproto_object_hook(v) if isinstance(v, dict) else v 124 + for k, v in obj.items()} 125 + 126 + return obj 127 + 128 + 129 + class TypedObject: 130 + """A typed object in the ATProto data model. 131 + 132 + This class represents an object with a $type field in the ATProto data model. 133 + 134 + Attributes: 135 + type: The type of the object. 136 + data: The data associated with the object. 137 + """ 138 + 139 + def __init__(self, type_name: str, data: Dict[str, Any]) -> None: 140 + """Initialize a typed object. 141 + 142 + Args: 143 + type_name: The type of the object. 144 + data: The data associated with the object. 145 + """ 146 + self.type_name = type_name 147 + self.data = data 148 + 149 + def __repr__(self) -> str: 150 + """Return a string representation of the typed object. 151 + 152 + Returns: 153 + A string representation of the typed object. 154 + """ 155 + return f"TypedObject(type_name={self.type_name!r}, data={self.data!r})" 156 + 157 + def __eq__(self, other: Any) -> bool: 158 + """Check if two typed objects are equal. 159 + 160 + Args: 161 + other: The object to compare with. 162 + 163 + Returns: 164 + True if the objects are equal, False otherwise. 165 + """ 166 + if not isinstance(other, TypedObject): 167 + return False 168 + return self.type_name == other.type_name and self.data == other.data 169 + 170 + def __atproto_json_encode__(self) -> Dict[str, Any]: 171 + """Encode the typed object to a JSON-serializable format. 172 + 173 + Returns: 174 + A JSON-serializable representation of the typed object. 175 + """ 176 + result = {"$type": self.type_name} 177 + result.update(self.data) 178 + return result
+82
src/atpasser/data/encoder.py
··· 1 + """ 2 + JSON encoder for ATProto data model. 3 + 4 + This module provides a JSON encoder that handles ATProto-specific data types, 5 + including bytes, CID links, and typed objects. 6 + """ 7 + 8 + import base64 9 + import json 10 + from typing import Any, Optional 11 + from cid import CIDv0, CIDv1 12 + 13 + 14 + class JsonEncoder(json.JSONEncoder): 15 + """A JSON encoder that supports ATProto data types. 16 + 17 + This encoder extends the standard JSON encoder to handle ATProto-specific 18 + data types, including bytes, CID links, and typed objects. 19 + 20 + Attributes: 21 + encoding (str): The encoding to use for string serialization. 22 + type_processor_registry: Registry for type-specific processors. 23 + """ 24 + 25 + def __init__( 26 + self, 27 + *, 28 + encoding: str = "utf-8", 29 + type_processor_registry: Optional[Any] = None, 30 + **kwargs: Any 31 + ) -> None: 32 + """Initialize the JSON encoder. 33 + 34 + Args: 35 + encoding: The encoding to use for string serialization. 36 + type_processor_registry: Registry for type-specific processors. 37 + **kwargs: Additional keyword arguments to pass to the parent class. 38 + """ 39 + super().__init__(**kwargs) 40 + self.encoding = encoding 41 + self.type_processor_registry = type_processor_registry 42 + 43 + def default(self, o: Any) -> Any: 44 + """Convert an object to a serializable format. 45 + 46 + Args: 47 + o: The object to serialize. 48 + 49 + Returns: 50 + A serializable representation of the object. 51 + 52 + Raises: 53 + TypeError: If the object is not serializable. 54 + """ 55 + if isinstance(o, bytes): 56 + # Handle bytes using RFC-4648 base64 encoding 57 + return {"$bytes": base64.b64encode(o).decode(self.encoding)} 58 + elif isinstance(o, (CIDv0, CIDv1)): 59 + # Handle CID objects 60 + return {"$link": str(o)} 61 + elif hasattr(o, "__atproto_json_encode__"): 62 + # Handle objects with custom ATProto encoding 63 + return o.__atproto_json_encode__() 64 + elif self.type_processor_registry is not None: 65 + # Try to find a type processor for this object 66 + obj_type_name = type(o).__name__ 67 + encoder = self.type_processor_registry.get_encoder(obj_type_name) 68 + if encoder is not None: 69 + result = encoder(o) 70 + # Add $type field if not already present 71 + if isinstance(result, dict) and "$type" not in result: 72 + result["$type"] = obj_type_name 73 + return result 74 + elif isinstance(o, dict): 75 + # Handle dictionaries recursively 76 + return {k: self.default(v) for k, v in o.items()} 77 + elif isinstance(o, (list, tuple)): 78 + # Handle lists and tuples recursively 79 + return [self.default(item) for item in o] 80 + else: 81 + # Use the parent class for other types 82 + return super().default(o)
+222
src/atpasser/data/hooks.py
··· 1 + """ 2 + Type hook system for ATProto JSON decoder. 3 + 4 + This module provides a decorator-based system for registering custom type handlers 5 + for objects with $type keys in the ATProto data model. 6 + """ 7 + 8 + import functools 9 + from typing import Any, Callable, Dict, Optional, TypeVar, Union 10 + 11 + # Type variable for the decorated function 12 + F = TypeVar('F', bound=Callable[..., Any]) 13 + 14 + 15 + class TypeHookRegistry: 16 + """Registry for type-specific hooks in the ATProto JSON decoder. 17 + 18 + This class maintains a registry of type-specific hooks that can be used 19 + to customize the decoding of objects with $type keys in the ATProto data model. 20 + 21 + Attributes: 22 + _handlers: Dictionary mapping type names to handler functions. 23 + """ 24 + 25 + def __init__(self) -> None: 26 + """Initialize the type hook registry.""" 27 + self._handlers: Dict[str, Callable[[Dict[str, Any]], Any]] = {} 28 + 29 + def register(self, type_name: str) -> Callable[[F], F]: 30 + """Register a type handler function. 31 + 32 + This method can be used as a decorator to register a function as a handler 33 + for a specific type. 34 + 35 + Args: 36 + type_name: The name of the type to handle. 37 + 38 + Returns: 39 + A decorator function that registers the decorated function as a handler. 40 + 41 + Example: 42 + >>> registry = TypeHookRegistry() 43 + >>> 44 + >>> @registry.register("app.bsky.feed.post") 45 + ... def handle_post(data: Dict[str, Any]) -> Any: 46 + ... return Post(**data) 47 + """ 48 + def decorator(func: F) -> F: 49 + self._handlers[type_name] = func 50 + return func 51 + 52 + return decorator 53 + 54 + def register_handler(self, type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None: 55 + """Register a type handler function directly. 56 + 57 + Args: 58 + type_name: The name of the type to handle. 59 + handler: The function to call when decoding objects of this type. 60 + 61 + Example: 62 + >>> registry = TypeHookRegistry() 63 + >>> 64 + >>> def handle_post(data: Dict[str, Any]) -> Any: 65 + ... return Post(**data) 66 + >>> 67 + >>> registry.register_handler("app.bsky.feed.post", handle_post) 68 + """ 69 + self._handlers[type_name] = handler 70 + 71 + def unregister(self, type_name: str) -> None: 72 + """Unregister a type handler function. 73 + 74 + Args: 75 + type_name: The name of the type to unregister. 76 + """ 77 + if type_name in self._handlers: 78 + del self._handlers[type_name] 79 + 80 + def get_handler(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]: 81 + """Get the handler function for a specific type. 82 + 83 + Args: 84 + type_name: The name of the type to get the handler for. 85 + 86 + Returns: 87 + The handler function for the specified type, or None if no handler 88 + is registered. 89 + """ 90 + return self._handlers.get(type_name) 91 + 92 + def has_handler(self, type_name: str) -> bool: 93 + """Check if a handler is registered for a specific type. 94 + 95 + Args: 96 + type_name: The name of the type to check. 97 + 98 + Returns: 99 + True if a handler is registered for the specified type, False otherwise. 100 + """ 101 + return type_name in self._handlers 102 + 103 + def clear(self) -> None: 104 + """Clear all registered handlers.""" 105 + self._handlers.clear() 106 + 107 + def get_registered_types(self) -> set: 108 + """Get the set of all registered type names. 109 + 110 + Returns: 111 + A set of all registered type names. 112 + """ 113 + return set(self._handlers.keys()) 114 + 115 + 116 + # Global registry instance 117 + _global_registry = TypeHookRegistry() 118 + 119 + 120 + def type_handler(type_name: str) -> Callable[[F], F]: 121 + """Register a global type handler function. 122 + 123 + This decorator registers a function as a global handler for a specific type 124 + in the ATProto data model. 125 + 126 + Args: 127 + type_name: The name of the type to handle. 128 + 129 + Returns: 130 + A decorator function that registers the decorated function as a handler. 131 + 132 + Example: 133 + >>> @type_handler("app.bsky.feed.post") 134 + ... def handle_post(data: Dict[str, Any]) -> Any: 135 + ... return Post(**data) 136 + """ 137 + return _global_registry.register(type_name) 138 + 139 + 140 + def get_global_registry() -> TypeHookRegistry: 141 + """Get the global type hook registry. 142 + 143 + Returns: 144 + The global TypeHookRegistry instance. 145 + """ 146 + return _global_registry 147 + 148 + 149 + def register_type_handler(type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None: 150 + """Register a global type handler function directly. 151 + 152 + Args: 153 + type_name: The name of the type to handle. 154 + handler: The function to call when decoding objects of this type. 155 + 156 + Example: 157 + >>> def handle_post(data: Dict[str, Any]) -> Any: 158 + ... return Post(**data) 159 + >>> 160 + >>> register_type_handler("app.bsky.feed.post", handle_post) 161 + """ 162 + _global_registry.register_handler(type_name, handler) 163 + 164 + 165 + def unregister_type_handler(type_name: str) -> None: 166 + """Unregister a global type handler function. 167 + 168 + Args: 169 + type_name: The name of the type to unregister. 170 + """ 171 + _global_registry.unregister(type_name) 172 + 173 + 174 + def get_type_handler(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]: 175 + """Get the global handler function for a specific type. 176 + 177 + Args: 178 + type_name: The name of the type to get the handler for. 179 + 180 + Returns: 181 + The handler function for the specified type, or None if no handler 182 + is registered. 183 + """ 184 + return _global_registry.get_handler(type_name) 185 + 186 + 187 + def has_type_handler(type_name: str) -> bool: 188 + """Check if a global handler is registered for a specific type. 189 + 190 + Args: 191 + type_name: The name of the type to check. 192 + 193 + Returns: 194 + True if a handler is registered for the specified type, False otherwise. 195 + """ 196 + return _global_registry.has_handler(type_name) 197 + 198 + 199 + def clear_type_handlers() -> None: 200 + """Clear all globally registered handlers.""" 201 + _global_registry.clear() 202 + 203 + 204 + def get_registered_types() -> set: 205 + """Get the set of all globally registered type names. 206 + 207 + Returns: 208 + A set of all registered type names. 209 + """ 210 + return _global_registry.get_registered_types() 211 + 212 + 213 + def create_registry() -> TypeHookRegistry: 214 + """Create a new type hook registry. 215 + 216 + This function creates a new, independent registry that can be used 217 + instead of the global registry. 218 + 219 + Returns: 220 + A new TypeHookRegistry instance. 221 + """ 222 + return TypeHookRegistry()
+504
src/atpasser/data/types.py
··· 1 + """ 2 + Type processor system for ATProto JSON decoder. 3 + 4 + This module provides an advanced type processor system that allows users to 5 + register custom type converters for objects with $type keys in the ATProto data model. 6 + """ 7 + 8 + import inspect 9 + from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union 10 + from .hooks import TypeHookRegistry 11 + 12 + # Type variable for the decorated class 13 + T = TypeVar('T') 14 + 15 + 16 + class TypeProcessor: 17 + """A type processor for ATProto JSON objects. 18 + 19 + This class represents a processor for a specific type in the ATProto data model. 20 + It contains information about how to convert JSON data to Python objects and 21 + vice versa. 22 + 23 + Attributes: 24 + type_name: The name of the type this processor handles. 25 + decoder: The function to decode JSON data to a Python object. 26 + encoder: The function to encode a Python object to JSON data. 27 + priority: The priority of this processor (higher values = higher priority). 28 + """ 29 + 30 + def __init__( 31 + self, 32 + type_name: str, 33 + decoder: Optional[Callable[[Dict[str, Any]], Any]] = None, 34 + encoder: Optional[Callable[[Any], Dict[str, Any]]] = None, 35 + priority: int = 0 36 + ) -> None: 37 + """Initialize a type processor. 38 + 39 + Args: 40 + type_name: The name of the type this processor handles. 41 + decoder: The function to decode JSON data to a Python object. 42 + encoder: The function to encode a Python object to JSON data. 43 + priority: The priority of this processor (higher values = higher priority). 44 + """ 45 + self.type_name = type_name 46 + self.decoder = decoder 47 + self.encoder = encoder 48 + self.priority = priority 49 + 50 + def decode(self, data: Dict[str, Any]) -> Any: 51 + """Decode JSON data to a Python object. 52 + 53 + Args: 54 + data: The JSON data to decode. 55 + 56 + Returns: 57 + The decoded Python object. 58 + 59 + Raises: 60 + ValueError: If no decoder is registered. 61 + """ 62 + if self.decoder is None: 63 + raise ValueError(f"No decoder registered for type {self.type_name}") 64 + return self.decoder(data) 65 + 66 + def encode(self, obj: Any) -> Dict[str, Any]: 67 + """Encode a Python object to JSON data. 68 + 69 + Args: 70 + obj: The Python object to encode. 71 + 72 + Returns: 73 + The encoded JSON data. 74 + 75 + Raises: 76 + ValueError: If no encoder is registered. 77 + """ 78 + if self.encoder is None: 79 + raise ValueError(f"No encoder registered for type {self.type_name}") 80 + return self.encoder(obj) 81 + 82 + 83 + class TypeProcessorRegistry: 84 + """Registry for type processors in the ATProto JSON decoder. 85 + 86 + This class maintains a registry of type processors that can be used 87 + to customize the encoding and decoding of objects with $type keys in 88 + the ATProto data model. 89 + 90 + Attributes: 91 + _processors: Dictionary mapping type names to processor lists. 92 + """ 93 + 94 + def __init__(self) -> None: 95 + """Initialize the type processor registry.""" 96 + self._processors: Dict[str, List[TypeProcessor]] = {} 97 + 98 + def register_processor(self, processor: TypeProcessor) -> None: 99 + """Register a type processor. 100 + 101 + Args: 102 + processor: The type processor to register. 103 + """ 104 + if processor.type_name not in self._processors: 105 + self._processors[processor.type_name] = [] 106 + 107 + self._processors[processor.type_name].append(processor) 108 + # Sort processors by priority (descending) 109 + self._processors[processor.type_name].sort(key=lambda p: p.priority, reverse=True) 110 + 111 + def register( 112 + self, 113 + type_name: str, 114 + priority: int = 0 115 + ) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]: 116 + """Register a type decoder function. 117 + 118 + This method can be used as a decorator to register a function as a decoder 119 + for a specific type. 120 + 121 + Args: 122 + type_name: The name of the type to handle. 123 + priority: The priority of this processor (higher values = higher priority). 124 + 125 + Returns: 126 + A decorator function that registers the decorated function as a decoder. 127 + 128 + Example: 129 + >>> registry = TypeProcessorRegistry() 130 + >>> 131 + >>> @registry.register("app.bsky.feed.post", priority=10) 132 + ... def decode_post(data: Dict[str, Any]) -> Any: 133 + ... return Post(**data) 134 + """ 135 + def decorator(func: Callable[[Dict[str, Any]], Any]) -> Callable[[Dict[str, Any]], Any]: 136 + processor = TypeProcessor(type_name, decoder=func, priority=priority) 137 + self.register_processor(processor) 138 + return func 139 + 140 + return decorator 141 + 142 + def register_encoder( 143 + self, 144 + type_name: str, 145 + priority: int = 0 146 + ) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]: 147 + """Register a type encoder function. 148 + 149 + This method can be used as a decorator to register a function as an encoder 150 + for a specific type. 151 + 152 + Args: 153 + type_name: The name of the type to handle. 154 + priority: The priority of this processor (higher values = higher priority). 155 + 156 + Returns: 157 + A decorator function that registers the decorated function as an encoder. 158 + 159 + Example: 160 + >>> registry = TypeProcessorRegistry() 161 + >>> 162 + >>> @registry.register_encoder("app.bsky.feed.post", priority=10) 163 + ... def encode_post(post: Post) -> Dict[str, Any]: 164 + ... return {"text": post.text, "createdAt": post.created_at} 165 + """ 166 + def decorator(func: Callable[[Any], Dict[str, Any]]) -> Callable[[Any], Dict[str, Any]]: 167 + # Check if a processor for this type already exists 168 + if type_name in self._processors: 169 + for processor in self._processors[type_name]: 170 + if processor.decoder is not None: 171 + # Update the existing processor with the encoder 172 + processor.encoder = func 173 + break 174 + else: 175 + # No decoder found, create a new processor 176 + processor = TypeProcessor(type_name, encoder=func, priority=priority) 177 + self.register_processor(processor) 178 + else: 179 + # No processor exists, create a new one 180 + processor = TypeProcessor(type_name, encoder=func, priority=priority) 181 + self.register_processor(processor) 182 + 183 + return func 184 + 185 + return decorator 186 + 187 + def register_class( 188 + self, 189 + type_name: str, 190 + priority: int = 0 191 + ) -> Callable[[Type[T]], Type[T]]: 192 + """Register a class for both encoding and decoding. 193 + 194 + This method can be used as a decorator to register a class for both 195 + encoding and decoding of a specific type. 196 + 197 + The class must have a class method `from_json` that takes a dictionary 198 + and returns an instance of the class, and an instance method `to_json` 199 + that returns a dictionary. 200 + 201 + Args: 202 + type_name: The name of the type to handle. 203 + priority: The priority of this processor (higher values = higher priority). 204 + 205 + Returns: 206 + A decorator function that registers the decorated class. 207 + 208 + Example: 209 + >>> registry = TypeProcessorRegistry() 210 + >>> 211 + >>> @registry.register_class("app.bsky.feed.post", priority=10) 212 + ... class Post: 213 + ... def __init__(self, text: str, created_at: str) -> None: 214 + ... self.text = text 215 + ... self.created_at = created_at 216 + ... 217 + ... @classmethod 218 + ... def from_json(cls, data: Dict[str, Any]) -> "Post": 219 + ... return cls(data["text"], data["createdAt"]) 220 + ... 221 + ... def to_json(self) -> Dict[str, Any]: 222 + ... return {"text": self.text, "createdAt": self.created_at} 223 + """ 224 + def decorator(cls: Type[T]) -> Type[T]: 225 + # Create decoder from class method 226 + if hasattr(cls, "from_json"): 227 + decoder = lambda data: getattr(cls, "from_json")(data) 228 + else: 229 + # Try to create a decoder from the constructor 230 + init_signature = inspect.signature(cls.__init__) 231 + if init_signature.parameters: 232 + # Create a decoder that passes the data as keyword arguments 233 + decoder = lambda data: cls(**data) 234 + else: 235 + raise ValueError(f"Class {cls.__name__} has no from_json method or compatible __init__") 236 + 237 + # Create encoder from instance method 238 + if hasattr(cls, "to_json"): 239 + encoder = lambda obj: obj.to_json() 240 + else: 241 + raise ValueError(f"Class {cls.__name__} has no to_json method") 242 + 243 + # Register the processor 244 + processor = TypeProcessor(type_name, decoder=decoder, encoder=encoder, priority=priority) 245 + self.register_processor(processor) 246 + 247 + return cls 248 + 249 + return decorator 250 + 251 + def unregister(self, type_name: str, priority: Optional[int] = None) -> None: 252 + """Unregister type processors. 253 + 254 + Args: 255 + type_name: The name of the type to unregister. 256 + priority: If specified, only unregister processors with this priority. 257 + """ 258 + if type_name in self._processors: 259 + if priority is not None: 260 + # Remove processors with the specified priority 261 + self._processors[type_name] = [ 262 + p for p in self._processors[type_name] if p.priority != priority 263 + ] 264 + else: 265 + # Remove all processors for this type 266 + del self._processors[type_name] 267 + 268 + def get_decoder(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]: 269 + """Get the decoder function for a specific type. 270 + 271 + Args: 272 + type_name: The name of the type to get the decoder for. 273 + 274 + Returns: 275 + The decoder function for the specified type, or None if no decoder 276 + is registered. 277 + """ 278 + if type_name in self._processors and self._processors[type_name]: 279 + # Return the decoder of the highest priority processor 280 + return self._processors[type_name][0].decoder 281 + return None 282 + 283 + def get_encoder(self, type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]: 284 + """Get the encoder function for a specific type. 285 + 286 + Args: 287 + type_name: The name of the type to get the encoder for. 288 + 289 + Returns: 290 + The encoder function for the specified type, or None if no encoder 291 + is registered. 292 + """ 293 + if type_name in self._processors and self._processors[type_name]: 294 + # Return the encoder of the highest priority processor 295 + return self._processors[type_name][0].encoder 296 + return None 297 + 298 + def has_processor(self, type_name: str) -> bool: 299 + """Check if a processor is registered for a specific type. 300 + 301 + Args: 302 + type_name: The name of the type to check. 303 + 304 + Returns: 305 + True if a processor is registered for the specified type, False otherwise. 306 + """ 307 + return type_name in self._processors and bool(self._processors[type_name]) 308 + 309 + def clear(self) -> None: 310 + """Clear all registered processors.""" 311 + self._processors.clear() 312 + 313 + def get_registered_types(self) -> set: 314 + """Get the set of all registered type names. 315 + 316 + Returns: 317 + A set of all registered type names. 318 + """ 319 + return set(self._processors.keys()) 320 + 321 + def to_hook_registry(self) -> TypeHookRegistry: 322 + """Convert this processor registry to a hook registry. 323 + 324 + This method creates a TypeHookRegistry that uses the decoders from 325 + this processor registry. 326 + 327 + Returns: 328 + A TypeHookRegistry with the same decoders as this processor registry. 329 + """ 330 + hook_registry = TypeHookRegistry() 331 + 332 + for type_name, processors in self._processors.items(): 333 + if processors and processors[0].decoder is not None: 334 + hook_registry.register_handler(type_name, processors[0].decoder) 335 + 336 + return hook_registry 337 + 338 + 339 + # Global registry instance 340 + _global_processor_registry = TypeProcessorRegistry() 341 + 342 + 343 + def register_type( 344 + type_name: str, 345 + priority: int = 0 346 + ) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]: 347 + """Register a global type decoder function. 348 + 349 + This decorator registers a function as a global decoder for a specific type 350 + in the ATProto data model. 351 + 352 + Args: 353 + type_name: The name of the type to handle. 354 + priority: The priority of this processor (higher values = higher priority). 355 + 356 + Returns: 357 + A decorator function that registers the decorated function as a decoder. 358 + 359 + Example: 360 + >>> @register_type("app.bsky.feed.post", priority=10) 361 + ... def decode_post(data: Dict[str, Any]) -> Any: 362 + ... return Post(**data) 363 + """ 364 + return _global_processor_registry.register(type_name, priority) 365 + 366 + 367 + def get_global_processor_registry() -> TypeProcessorRegistry: 368 + """Get the global type processor registry. 369 + 370 + Returns: 371 + The global TypeProcessorRegistry instance. 372 + """ 373 + return _global_processor_registry 374 + 375 + 376 + def register_type_encoder( 377 + type_name: str, 378 + priority: int = 0 379 + ) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]: 380 + """Register a global type encoder function. 381 + 382 + This decorator registers a function as a global encoder for a specific type 383 + in the ATProto data model. 384 + 385 + Args: 386 + type_name: The name of the type to handle. 387 + priority: The priority of this processor (higher values = higher priority). 388 + 389 + Returns: 390 + A decorator function that registers the decorated function as an encoder. 391 + 392 + Example: 393 + >>> @register_type_encoder("app.bsky.feed.post", priority=10) 394 + ... def encode_post(post: Post) -> Dict[str, Any]: 395 + ... return {"text": post.text, "createdAt": post.created_at} 396 + """ 397 + return _global_processor_registry.register_encoder(type_name, priority) 398 + 399 + 400 + def register_type_class( 401 + type_name: str, 402 + priority: int = 0 403 + ) -> Callable[[Type[T]], Type[T]]: 404 + """Register a class for both global encoding and decoding. 405 + 406 + This decorator registers a class for both encoding and decoding of a specific type 407 + in the ATProto data model. 408 + 409 + Args: 410 + type_name: The name of the type to handle. 411 + priority: The priority of this processor (higher values = higher priority). 412 + 413 + Returns: 414 + A decorator function that registers the decorated class. 415 + 416 + Example: 417 + >>> @register_type_class("app.bsky.feed.post", priority=10) 418 + ... class Post: 419 + ... def __init__(self, text: str, created_at: str) -> None: 420 + ... self.text = text 421 + ... self.created_at = created_at 422 + ... 423 + ... @classmethod 424 + ... def from_json(cls, data: Dict[str, Any]) -> "Post": 425 + ... return cls(data["text"], data["createdAt"]) 426 + ... 427 + ... def to_json(self) -> Dict[str, Any]: 428 + ... return {"text": self.text, "createdAt": self.created_at} 429 + """ 430 + return _global_processor_registry.register_class(type_name, priority) 431 + 432 + 433 + def unregister_type(type_name: str, priority: Optional[int] = None) -> None: 434 + """Unregister global type processors. 435 + 436 + Args: 437 + type_name: The name of the type to unregister. 438 + priority: If specified, only unregister processors with this priority. 439 + """ 440 + _global_processor_registry.unregister(type_name, priority) 441 + 442 + 443 + def get_type_decoder(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]: 444 + """Get the global decoder function for a specific type. 445 + 446 + Args: 447 + type_name: The name of the type to get the decoder for. 448 + 449 + Returns: 450 + The decoder function for the specified type, or None if no decoder 451 + is registered. 452 + """ 453 + return _global_processor_registry.get_decoder(type_name) 454 + 455 + 456 + def get_type_encoder(type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]: 457 + """Get the global encoder function for a specific type. 458 + 459 + Args: 460 + type_name: The name of the type to get the encoder for. 461 + 462 + Returns: 463 + The encoder function for the specified type, or None if no encoder 464 + is registered. 465 + """ 466 + return _global_processor_registry.get_encoder(type_name) 467 + 468 + 469 + def has_type_processor(type_name: str) -> bool: 470 + """Check if a global processor is registered for a specific type. 471 + 472 + Args: 473 + type_name: The name of the type to check. 474 + 475 + Returns: 476 + True if a processor is registered for the specified type, False otherwise. 477 + """ 478 + return _global_processor_registry.has_processor(type_name) 479 + 480 + 481 + def clear_type_processors() -> None: 482 + """Clear all globally registered processors.""" 483 + _global_processor_registry.clear() 484 + 485 + 486 + def get_registered_types() -> set: 487 + """Get the set of all globally registered type names. 488 + 489 + Returns: 490 + A set of all registered type names. 491 + """ 492 + return _global_processor_registry.get_registered_types() 493 + 494 + 495 + def create_processor_registry() -> TypeProcessorRegistry: 496 + """Create a new type processor registry. 497 + 498 + This function creates a new, independent registry that can be used 499 + instead of the global registry. 500 + 501 + Returns: 502 + A new TypeProcessorRegistry instance. 503 + """ 504 + return TypeProcessorRegistry()
+339
src/atpasser/data/wrapper.py
··· 1 + """ 2 + JSON wrapper functions for ATProto data model. 3 + 4 + This module provides wrapper functions that mirror the standard json module 5 + but with support for ATProto-specific data types. 6 + """ 7 + 8 + import json 9 + import io 10 + from typing import Any, Callable, Dict, Optional, TextIO, Union 11 + from .encoder import JsonEncoder 12 + from .decoder import JsonDecoder 13 + from .hooks import TypeHookRegistry 14 + from .types import TypeProcessorRegistry 15 + 16 + 17 + def dump( 18 + obj: Any, 19 + fp: TextIO, 20 + *, 21 + skipkeys: bool = False, 22 + ensure_ascii: bool = True, 23 + check_circular: bool = True, 24 + allow_nan: bool = True, 25 + cls: Optional[type[JsonEncoder]] = None, 26 + indent: Optional[Union[int, str]] = None, 27 + separators: Optional[tuple[str, str]] = None, 28 + default: Optional[Callable[[Any], Any]] = None, 29 + sort_keys: bool = False, 30 + encoding: str = "utf-8", 31 + type_processor_registry: Optional[TypeProcessorRegistry] = None, 32 + **kwargs: Any 33 + ) -> None: 34 + """Serialize obj as a JSON formatted stream to fp. 35 + 36 + This function is similar to json.dump() but supports ATProto-specific 37 + data types, including bytes, CID links, and typed objects. 38 + 39 + Args: 40 + obj: The object to serialize. 41 + fp: A file-like object with a write() method. 42 + skipkeys: If True, dict keys that are not basic types (str, int, float, 43 + bool, None) will be skipped instead of raising a TypeError. 44 + ensure_ascii: If True, the output is guaranteed to have all incoming 45 + non-ASCII characters escaped. If False, these characters will be 46 + output as-is. 47 + check_circular: If True, circular references will be checked and 48 + a CircularReferenceError will be raised if one is found. 49 + allow_nan: If True, NaN, Infinity, and -Infinity will be encoded as 50 + such. This behavior is not JSON specification compliant, but it 51 + is consistent with most JavaScript based encoders and decoders. 52 + Otherwise, it will raise a ValueError. 53 + cls: A custom JSONEncoder subclass. If not specified, JsonEncoder is used. 54 + indent: If indent is a non-negative integer or string, then JSON array 55 + elements and object members will be pretty-printed with that indent 56 + level. An indent level of 0, negative, or "" will only insert newlines. 57 + None (the default) selects the most compact representation. 58 + separators: If specified, separators should be an (item_separator, key_separator) 59 + tuple. The default is (', ', ': ') if indent is None and (',', ': ') otherwise. 60 + To get the most compact JSON representation, you should specify (',', ':') 61 + to eliminate whitespace. 62 + default: If specified, default should be a function that gets called for 63 + objects that can't otherwise be serialized. It should return a JSON 64 + encodable version of the object or raise a TypeError. 65 + sort_keys: If sort_keys is True, then the output of dictionaries will be 66 + sorted by key. 67 + encoding: The encoding to use for string serialization. 68 + type_processor_registry: Registry for type-specific processors. 69 + **kwargs: Additional keyword arguments to pass to the JSON encoder. 70 + """ 71 + if cls is None: 72 + cls = JsonEncoder 73 + 74 + # Use the global type processor registry if none is provided 75 + if type_processor_registry is None: 76 + from .types import get_global_processor_registry 77 + type_processor_registry = get_global_processor_registry() 78 + 79 + # Create an encoder instance with the specified encoding and type processor registry 80 + encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs) 81 + 82 + # Use the standard json.dump with our custom encoder 83 + json.dump( 84 + obj, 85 + fp, 86 + skipkeys=skipkeys, 87 + ensure_ascii=ensure_ascii, 88 + check_circular=check_circular, 89 + allow_nan=allow_nan, 90 + cls=cls, 91 + indent=indent, 92 + separators=separators, 93 + default=default, 94 + sort_keys=sort_keys, 95 + **kwargs 96 + ) 97 + 98 + 99 + def dumps( 100 + obj: Any, 101 + *, 102 + skipkeys: bool = False, 103 + ensure_ascii: bool = True, 104 + check_circular: bool = True, 105 + allow_nan: bool = True, 106 + cls: Optional[type[JsonEncoder]] = None, 107 + indent: Optional[Union[int, str]] = None, 108 + separators: Optional[tuple[str, str]] = None, 109 + default: Optional[Callable[[Any], Any]] = None, 110 + sort_keys: bool = False, 111 + encoding: str = "utf-8", 112 + type_processor_registry: Optional[TypeProcessorRegistry] = None, 113 + **kwargs: Any 114 + ) -> str: 115 + """Serialize obj to a JSON formatted string. 116 + 117 + This function is similar to json.dumps() but supports ATProto-specific 118 + data types, including bytes, CID links, and typed objects. 119 + 120 + Args: 121 + obj: The object to serialize. 122 + skipkeys: If True, dict keys that are not basic types (str, int, float, 123 + bool, None) will be skipped instead of raising a TypeError. 124 + ensure_ascii: If True, the output is guaranteed to have all incoming 125 + non-ASCII characters escaped. If False, these characters will be 126 + output as-is. 127 + check_circular: If True, circular references will be checked and 128 + a CircularReferenceError will be raised if one is found. 129 + allow_nan: If True, NaN, Infinity, and -Infinity will be encoded as 130 + such. This behavior is not JSON specification compliant, but it 131 + is consistent with most JavaScript based encoders and decoders. 132 + Otherwise, it will raise a ValueError. 133 + cls: A custom JSONEncoder subclass. If not specified, JsonEncoder is used. 134 + indent: If indent is a non-negative integer or string, then JSON array 135 + elements and object members will be pretty-printed with that indent 136 + level. An indent level of 0, negative, or "" will only insert newlines. 137 + None (the default) selects the most compact representation. 138 + separators: If specified, separators should be an (item_separator, key_separator) 139 + tuple. The default is (', ', ': ') if indent is None and (',', ': ') otherwise. 140 + To get the most compact JSON representation, you should specify (',', ':') 141 + to eliminate whitespace. 142 + default: If specified, default should be a function that gets called for 143 + objects that can't otherwise be serialized. It should return a JSON 144 + encodable version of the object or raise a TypeError. 145 + sort_keys: If sort_keys is True, then the output of dictionaries will be 146 + sorted by key. 147 + encoding: The encoding to use for string serialization. 148 + type_processor_registry: Registry for type-specific processors. 149 + **kwargs: Additional keyword arguments to pass to the JSON encoder. 150 + 151 + Returns: 152 + A JSON formatted string. 153 + """ 154 + if cls is None: 155 + cls = JsonEncoder 156 + 157 + # Create an encoder instance with the specified encoding and type processor registry 158 + encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs) 159 + 160 + # Use the standard json.dumps with our custom encoder 161 + return json.dumps( 162 + obj, 163 + skipkeys=skipkeys, 164 + ensure_ascii=ensure_ascii, 165 + check_circular=check_circular, 166 + allow_nan=allow_nan, 167 + cls=cls, 168 + indent=indent, 169 + separators=separators, 170 + default=default, 171 + sort_keys=sort_keys, 172 + **kwargs 173 + ) 174 + 175 + 176 + def load( 177 + fp: TextIO, 178 + *, 179 + cls: Optional[type[JsonDecoder]] = None, 180 + object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None, 181 + parse_float: Optional[Callable[[str], Any]] = None, 182 + parse_int: Optional[Callable[[str], Any]] = None, 183 + parse_constant: Optional[Callable[[str], Any]] = None, 184 + object_pairs_hook: Optional[Callable[[list[tuple[str, Any]]], Any]] = None, 185 + type_hook_registry: Optional[TypeHookRegistry] = None, 186 + type_processor_registry: Optional[TypeProcessorRegistry] = None, 187 + encoding: str = "utf-8", 188 + **kwargs: Any 189 + ) -> Any: 190 + """Deserialize fp (a .read()-supporting text file or binary file containing 191 + a JSON document) to a Python object. 192 + 193 + This function is similar to json.load() but supports ATProto-specific 194 + data types, including bytes, CID links, and typed objects. 195 + 196 + Args: 197 + fp: A .read()-supporting text file or binary file containing a JSON document. 198 + cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used. 199 + object_hook: Optional function that will be called with the result of 200 + every JSON object decoded and its return value will be used in place 201 + of the given dict. 202 + parse_float: Optional function that will be called with the string of 203 + every JSON float to be decoded. By default, this is equivalent to 204 + float(num_str). This can be used to use another datatype or parser 205 + for JSON floats (e.g. decimal.Decimal). 206 + parse_int: Optional function that will be called with the string of 207 + every JSON int to be decoded. By default, this is equivalent to 208 + int(num_str). This can be used to use another datatype or parser 209 + for JSON integers (e.g. float). 210 + parse_constant: Optional function that will be called with the string of 211 + every JSON constant to be decoded. By default, this is equivalent to 212 + constant_mapping[constant_str]. This can be used to use another 213 + datatype or parser for JSON constants (e.g. decimal.Decimal). 214 + object_pairs_hook: Optional function that will be called with the result 215 + of every JSON object decoded with an ordered list of pairs. The return 216 + value of object_pairs_hook will be used instead of the dict. This 217 + feature can be used to implement custom decoders. If object_hook is 218 + also defined, the object_pairs_hook takes priority. 219 + type_hook_registry: Registry for type-specific hooks. 220 + type_processor_registry: Registry for type-specific processors. 221 + encoding: The encoding to use for string deserialization. 222 + **kwargs: Additional keyword arguments to pass to the JSON decoder. 223 + 224 + Returns: 225 + A Python object. 226 + """ 227 + if cls is None: 228 + cls = JsonDecoder 229 + 230 + # Use the global type hook registry if none is provided 231 + if type_hook_registry is None and type_processor_registry is None: 232 + from .hooks import get_global_registry 233 + type_hook_registry = get_global_registry() 234 + elif type_processor_registry is not None: 235 + # Convert the type processor registry to a hook registry 236 + type_hook_registry = type_processor_registry.to_hook_registry() 237 + 238 + # Create a decoder instance with the specified parameters 239 + decoder = cls( 240 + object_hook=object_hook, 241 + type_hook_registry=type_hook_registry, 242 + encoding=encoding, 243 + **kwargs 244 + ) 245 + 246 + # Use the standard json.load with our custom decoder 247 + return json.load( 248 + fp, 249 + cls=cls, 250 + object_hook=object_hook, 251 + parse_float=parse_float, 252 + parse_int=parse_int, 253 + parse_constant=parse_constant, 254 + object_pairs_hook=object_pairs_hook, 255 + **kwargs 256 + ) 257 + 258 + 259 + def loads( 260 + s: Union[str, bytes], 261 + *, 262 + cls: Optional[type[JsonDecoder]] = None, 263 + object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None, 264 + parse_float: Optional[Callable[[str], Any]] = None, 265 + parse_int: Optional[Callable[[str], Any]] = None, 266 + parse_constant: Optional[Callable[[str], Any]] = None, 267 + object_pairs_hook: Optional[Callable[[list[tuple[str, Any]]], Any]] = None, 268 + type_hook_registry: Optional[TypeHookRegistry] = None, 269 + type_processor_registry: Optional[TypeProcessorRegistry] = None, 270 + encoding: str = "utf-8", 271 + **kwargs: Any 272 + ) -> Any: 273 + """Deserialize s (a str, bytes or bytearray instance containing a JSON document) 274 + to a Python object. 275 + 276 + This function is similar to json.loads() but supports ATProto-specific 277 + data types, including bytes, CID links, and typed objects. 278 + 279 + Args: 280 + s: A str, bytes or bytearray instance containing a JSON document. 281 + cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used. 282 + object_hook: Optional function that will be called with the result of 283 + every JSON object decoded and its return value will be used in place 284 + of the given dict. 285 + parse_float: Optional function that will be called with the string of 286 + every JSON float to be decoded. By default, this is equivalent to 287 + float(num_str). This can be used to use another datatype or parser 288 + for JSON floats (e.g. decimal.Decimal). 289 + parse_int: Optional function that will be called with the string of 290 + every JSON int to be decoded. By default, this is equivalent to 291 + int(num_str). This can be used to use another datatype or parser 292 + for JSON integers (e.g. float). 293 + parse_constant: Optional function that will be called with the string of 294 + every JSON constant to be decoded. By default, this is equivalent to 295 + constant_mapping[constant_str]. This can be used to use another 296 + datatype or parser for JSON constants (e.g. decimal.Decimal). 297 + object_pairs_hook: Optional function that will be called with the result 298 + of every JSON object decoded with an ordered list of pairs. The return 299 + value of object_pairs_hook will be used instead of the dict. This 300 + feature can be used to implement custom decoders. If object_hook is 301 + also defined, the object_pairs_hook takes priority. 302 + type_hook_registry: Registry for type-specific hooks. 303 + type_processor_registry: Registry for type-specific processors. 304 + encoding: The encoding to use for string deserialization. 305 + **kwargs: Additional keyword arguments to pass to the JSON decoder. 306 + 307 + Returns: 308 + A Python object. 309 + """ 310 + if cls is None: 311 + cls = JsonDecoder 312 + 313 + # Use the global type hook registry if none is provided 314 + if type_hook_registry is None and type_processor_registry is None: 315 + from .hooks import get_global_registry 316 + type_hook_registry = get_global_registry() 317 + elif type_processor_registry is not None: 318 + # Convert the type processor registry to a hook registry 319 + type_hook_registry = type_processor_registry.to_hook_registry() 320 + 321 + # Create a decoder instance with the specified parameters 322 + decoder = cls( 323 + object_hook=object_hook, 324 + type_hook_registry=type_hook_registry, 325 + encoding=encoding, 326 + **kwargs 327 + ) 328 + 329 + # Use the standard json.loads with our custom decoder 330 + return json.loads( 331 + s, 332 + cls=cls, 333 + object_hook=object_hook, 334 + parse_float=parse_float, 335 + parse_int=parse_int, 336 + parse_constant=parse_constant, 337 + object_pairs_hook=object_pairs_hook, 338 + **kwargs 339 + )
-4
tests/__init__.py
··· 1 - if __name__ != "__main__": 2 - raise Exception("name != main") 3 - 4 - import _strings
-179
tests/_strings.py
··· 1 - from atpasser import did, handle, nsid, rKey, uri 2 - 3 - 4 - testStrings, testMethods = {}, {} 5 - 6 - 7 - testStrings[ 8 - "did" 9 - 10 - ] = """did:plc:z72i7hdynmk6r22z27h6tvur 11 - 12 - did:web:blueskyweb.xyz 13 - 14 - did:method:val:two 15 - 16 - did:m:v 17 - 18 - did:method::::val 19 - 20 - did:method:-:_:. 21 - 22 - did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N 23 - 24 - did:METHOD:val 25 - 26 - did:m123:val 27 - 28 - DID:method:val 29 - did:method: 30 - 31 - did:method:val/two 32 - 33 - did:method:val?two 34 - 35 - did:method:val#two""" 36 - 37 - testMethods["did"] = did.DID 38 - 39 - 40 - testStrings[ 41 - "handle" 42 - 43 - ] = """jay.bsky.social 44 - 45 - 8.cn 46 - 47 - name.t--t 48 - 49 - XX.LCS.MIT.EDU 50 - a.co 51 - 52 - xn--notarealidn.com 53 - 54 - xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s 55 - 56 - xn--ls8h.test 57 - example.t 58 - 59 - jo@hn.test 60 - 61 - 💩.tes 62 - t 63 - john..test 64 - 65 - xn--bcher-.tld 66 - 67 - john.0 68 - 69 - cn.8 70 - 71 - www.masełkowski.pl.com 72 - 73 - org 74 - 75 - name.org. 76 - 77 - 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 78 - laptop.local 79 - 80 - blah.arpa""" 81 - 82 - testMethods["handle"] = handle.Handle 83 - 84 - 85 - testStrings[ 86 - "nsid" 87 - 88 - ] = """com.example.fooBar 89 - 90 - net.users.bob.ping 91 - 92 - a-0.b-1.c 93 - 94 - a.b.c 95 - 96 - com.example.fooBarV2 97 - 98 - cn.8.lex.stuff 99 - 100 - com.exa💩ple.thin 101 - com.example 102 - 103 - com.example.3""" 104 - 105 - testMethods["nsid"] = nsid.NSID 106 - 107 - 108 - testStrings[ 109 - 110 - "rkey" 111 - 112 - ] = """3jui7kd54zh2y 113 - self 114 - example.com 115 - 116 - ~1.2-3_ 117 - 118 - dHJ1ZQ 119 - pre:fix 120 - 121 - _ 122 - 123 - alpha/beta 124 - . 125 - .. 126 - 127 - #extra 128 - 129 - @handle 130 - 131 - any space 132 - 133 - any+space 134 - 135 - number[3] 136 - 137 - number(3) 138 - 139 - "quote" 140 - 141 - dHJ1ZQ==""" 142 - 143 - testMethods["rkey"] = rKey.RKey 144 - 145 - 146 - testStrings[ 147 - "uri" 148 - 149 - ] = """at://foo.com/com.example.foo/123 150 - 151 - at://foo.com/example/123 152 - 153 - at://computer 154 - 155 - at://example.com:3000 156 - 157 - at://foo.com/ 158 - 159 - at://user:pass@foo.com""" 160 - 161 - testMethods["uri"] = uri.URI 162 - 163 - 164 - for item in testMethods: 165 - 166 - print(f"START TEST {item}") 167 - 168 - for value in testStrings[item].splitlines(): 169 - 170 - print(f"Value: {value}") 171 - 172 - try: 173 - 174 - print(f"str(): {str(testMethods[item](value))}") 175 - 176 - except Exception as e: 177 - 178 - print(f"× {e}") 179 -