rebuild data model

+91 -1
poetry.lock
···
]
[[package]]
+
name = "colorama"
+
version = "0.4.6"
+
description = "Cross-platform colored terminal text."
+
optional = false
+
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+
groups = ["main"]
+
markers = "sys_platform == \"win32\""
+
files = [
+
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+
]
+
+
[[package]]
name = "cryptography"
version = "45.0.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
···
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
+
[[package]]
+
name = "iniconfig"
+
version = "2.1.0"
+
description = "brain-dead simple config-ini parsing"
+
optional = false
+
python-versions = ">=3.8"
+
groups = ["main"]
+
files = [
+
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
+
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+
]
[[package]]
name = "jsonpath-ng"
···
]
[[package]]
+
name = "packaging"
+
version = "25.0"
+
description = "Core utilities for Python packages"
+
optional = false
+
python-versions = ">=3.8"
+
groups = ["main"]
+
files = [
+
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
+
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
+
]
+
+
[[package]]
+
name = "pluggy"
+
version = "1.6.0"
+
description = "plugin and hook calling mechanisms for python"
+
optional = false
+
python-versions = ">=3.9"
+
groups = ["main"]
+
files = [
+
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+
]
+
+
[package.extras]
+
dev = ["pre-commit", "tox"]
+
testing = ["coverage", "pytest", "pytest-benchmark"]
+
+
[[package]]
name = "ply"
version = "3.11"
description = "Python Lex & Yacc"
···
]
[[package]]
+
name = "pygments"
+
version = "2.19.2"
+
description = "Pygments is a syntax highlighting package written in Python."
+
optional = false
+
python-versions = ">=3.8"
+
groups = ["main"]
+
files = [
+
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
+
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
+
]
+
+
[package.extras]
+
windows-terminal = ["colorama (>=0.4.6)"]
+
+
[[package]]
name = "pyld"
version = "2.0.4"
description = "Python implementation of the JSON-LD API"
···
sha3 = ["pysha3"]
[[package]]
+
name = "pytest"
+
version = "8.4.2"
+
description = "pytest: simple powerful testing with Python"
+
optional = false
+
python-versions = ">=3.9"
+
groups = ["main"]
+
files = [
+
{file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"},
+
{file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"},
+
]
+
+
[package.dependencies]
+
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+
iniconfig = ">=1"
+
packaging = ">=20"
+
pluggy = ">=1.5,<2"
+
pygments = ">=2.7.2"
+
+
[package.extras]
+
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+
[[package]]
name = "python-baseconv"
version = "1.2.2"
description = "Convert numbers from base 10 integers to base X strings and back again."
···
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
-
content-hash = "5f4e5fd166bf6b2010ec5acaf545a0cfe376dcc5437530f3add4b58f10ce439f"
+
content-hash = "959bc6b01857f65b67d47c12c46abc7af95aecf9913baf195392232500f2253a"
+1
pyproject.toml
···
"jsonpath-ng (>=1.7.0,<2.0.0)", # for URI fragment support
"cryptography (>=45.0.7,<46.0.0)", # just keep it
"langcodes (>=3.5.0,<4.0.0)", # language codes support
+
"pytest (>=8.4.2,<9.0.0)",
]
license = "MIT OR Apache-2.0"
license-files = ["LICEN[CS]E.*"]
+49 -1
src/atpasser/data/__init__.py
···
-
from ._wrapper import *
+
"""
+
JSON module wrapper for ATProto data model.
+
+
This module provides a JSON encoder and decoder that handle ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
"""
+
+
from .encoder import JsonEncoder
+
from .decoder import JsonDecoder, TypedObject
+
from .hooks import TypeHookRegistry, type_handler
+
from .types import (
+
TypeProcessor,
+
TypeProcessorRegistry,
+
register_type,
+
register_type_encoder,
+
register_type_class,
+
unregister_type,
+
get_type_decoder,
+
get_type_encoder,
+
has_type_processor,
+
clear_type_processors,
+
get_registered_types,
+
create_processor_registry,
+
)
+
from .wrapper import dump, dumps, load, loads
+
+
__all__ = [
+
"JsonEncoder",
+
"JsonDecoder",
+
"TypedObject",
+
"TypeHookRegistry",
+
"type_handler",
+
"TypeProcessor",
+
"TypeProcessorRegistry",
+
"register_type",
+
"register_type_encoder",
+
"register_type_class",
+
"unregister_type",
+
"get_type_decoder",
+
"get_type_encoder",
+
"has_type_processor",
+
"clear_type_processors",
+
"get_registered_types",
+
"create_processor_registry",
+
"dump",
+
"dumps",
+
"load",
+
"loads",
+
]
-76
src/atpasser/data/_data.py
···
-
import base64
-
from cid import CIDv0, CIDv1, cid, make_cid
-
import json
-
-
-
class Data:
-
"""
-
A class representing data with "$type" key.
-
-
Attributes:
-
type (str): The type of the data.
-
json (str): Original object in JSON format.
-
"""
-
-
def __init__(self, dataType: str, json: str = "{}") -> None:
-
"""
-
Initalizes data object.
-
-
Parameters:
-
type (str): The type of the data.
-
json (str): Original object in JSON format.
-
"""
-
self.type = dataType
-
self.json = json
-
-
def data(self):
-
"""
-
Loads data as a Python-friendly format.
-
-
Returns:
-
dict: Converted data from JSON object.
-
"""
-
return json.loads(self.json, object_hook=dataHook)
-
-
-
def dataHook(data: dict):
-
"""
-
Treated as `JSONDecoder`'s `object_hook`
-
-
Parameters:
-
data: data in format that `JSONDecoder` like ;)
-
"""
-
if "$bytes" in data:
-
return base64.b64decode(data["$bytes"])
-
elif "$link" in data:
-
return make_cid(data["$link"])
-
elif "$type" in data:
-
dataType = data["$type"]
-
del data["$type"]
-
return Data(dataType, json.dumps(data))
-
else:
-
return data
-
-
-
def _convertDataToFakeJSON(data):
-
if isinstance(data, bytes):
-
return {"$bytes": base64.b64encode(data)}
-
elif isinstance(data, (CIDv0, CIDv1)):
-
return {"link": data.encode()}
-
elif isinstance(data, dict):
-
for item in data:
-
data[item] = _convertDataToFakeJSON(data[item])
-
elif isinstance(data, (tuple, list, set)):
-
return [_convertDataToFakeJSON(item) for item in data]
-
else:
-
return data
-
-
-
class DataEncoder(json.JSONEncoder):
-
"""
-
A superset of `JSONEncoder` to support ATProto data.
-
"""
-
-
def default(self, o):
-
result = _convertDataToFakeJSON(o)
-
return super().default(result)
-61
src/atpasser/data/_wrapper.py
···
-
from json import loads
-
from typing import Callable, Any
-
from ._data import *
-
import functools
-
-
# Pyright did the whole job. Thank it.
-
-
-
class DataDecoder(json.JSONDecoder):
-
"""
-
A superset of `JSONDecoder` to support ATProto data.
-
"""
-
-
def __init__(
-
self,
-
*,
-
object_hook: Callable[[dict[str, Any]], Any] | None = dataHook,
-
parse_float: Callable[[str], Any] | None = None,
-
parse_int: Callable[[str], Any] | None = None,
-
parse_constant: Callable[[str], Any] | None = None,
-
strict: bool = True,
-
object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None,
-
) -> None:
-
super().__init__(
-
object_hook=object_hook,
-
parse_float=parse_float,
-
parse_int=parse_int,
-
parse_constant=parse_constant,
-
strict=strict,
-
object_pairs_hook=object_pairs_hook,
-
)
-
-
-
# Screw it. I have to make 4 `json`-like functions.
-
-
-
def _dataDecoratorForDump(func):
-
@functools.wraps(func)
-
def wrapper(obj, *args, **kwargs):
-
kwargs.setdefault("cls", DataEncoder)
-
return func(obj, *args, **kwargs)
-
-
return wrapper
-
-
-
def _dataDecoratorForLoad(func):
-
@functools.wraps(func)
-
def wrapper(obj, *args, **kwargs):
-
kwargs.setdefault("cls", DataDecoder)
-
return func(obj, *args, **kwargs)
-
-
return wrapper
-
-
-
dump = _dataDecoratorForDump(json.dump)
-
dumps = _dataDecoratorForDump(json.dumps)
-
load = _dataDecoratorForLoad(json.load)
-
loads = _dataDecoratorForLoad(json.loads)
-
"""
-
Wrapper of the JSON functions to support ATProto data.
-
"""
-137
src/atpasser/data/cbor.py
···
-
from datetime import tzinfo
-
import typing
-
import cbor2
-
import cid
-
-
from .data import dataHook, Data
-
-
-
def tagHook(decoder: cbor2.CBORDecoder, tag: cbor2.CBORTag, shareable_index=None):
-
"""
-
A simple tag hook for CID support.
-
"""
-
return cid.from_bytes(tag.value) if tag.tag == 42 else tag
-
-
-
class CBOREncoder(cbor2.CBOREncoder):
-
"""
-
Wrapper of cbor2.CBOREncoder.
-
"""
-
-
def __init__(
-
self,
-
fp: typing.IO[bytes],
-
datetime_as_timestamp: bool = False,
-
timezone: tzinfo | None = None,
-
value_sharing: bool = False,
-
default: (
-
typing.Callable[[cbor2.CBOREncoder, typing.Any], typing.Any] | None
-
) = None,
-
canonical: bool = False,
-
date_as_datetime: bool = False,
-
string_referencing: bool = False,
-
indefinite_containers: bool = False,
-
):
-
super().__init__(
-
fp,
-
datetime_as_timestamp,
-
timezone,
-
value_sharing,
-
default,
-
canonical,
-
date_as_datetime,
-
string_referencing,
-
indefinite_containers,
-
)
-
-
@cbor2.shareable_encoder
-
def cidOrDataEncoder(self: cbor2.CBOREncoder, value: cid.CIDv0 | cid.CIDv1 | Data):
-
"""
-
Encode CID or Data to CBOR Tag.
-
"""
-
if isinstance(value, (cid.CIDv0, cid.CIDv1)):
-
self.encode(cbor2.CBORTag(42, value.encode()))
-
elif isinstance(value, Data):
-
self.encode(value.data())
-
-
-
def _cborObjectHook(decoder: cbor2.CBORDecoder, value):
-
return dataHook(value)
-
-
-
class CBORDecoder(cbor2.CBORDecoder):
-
"""
-
Wrapper of cbor2.CBORDecoder.
-
"""
-
-
def __init__(
-
self,
-
fp: typing.IO[bytes],
-
tag_hook: (
-
typing.Callable[[cbor2.CBORDecoder, cbor2.CBORTag], typing.Any] | None
-
) = tagHook,
-
object_hook: (
-
typing.Callable[
-
[cbor2.CBORDecoder, dict[typing.Any, typing.Any]], typing.Any
-
]
-
| None
-
) = _cborObjectHook,
-
str_errors: typing.Literal["strict", "error", "replace"] = "strict",
-
):
-
super().__init__(fp, tag_hook, object_hook, str_errors)
-
-
-
# Make things for CBOR again.
-
-
from io import BytesIO
-
-
-
def dumps(
-
obj: object,
-
datetime_as_timestamp: bool = False,
-
timezone: tzinfo | None = None,
-
value_sharing: bool = False,
-
default: typing.Callable[[cbor2.CBOREncoder, typing.Any], typing.Any] | None = None,
-
canonical: bool = False,
-
date_as_datetime: bool = False,
-
string_referencing: bool = False,
-
indefinite_containers: bool = False,
-
) -> bytes:
-
with BytesIO() as fp:
-
CBOREncoder(
-
fp,
-
datetime_as_timestamp=datetime_as_timestamp,
-
timezone=timezone,
-
value_sharing=value_sharing,
-
default=default,
-
canonical=canonical,
-
date_as_datetime=date_as_datetime,
-
string_referencing=string_referencing,
-
indefinite_containers=indefinite_containers,
-
).encode(obj)
-
return fp.getvalue()
-
-
-
def dump(
-
obj: object,
-
fp: typing.IO[bytes],
-
datetime_as_timestamp: bool = False,
-
timezone: tzinfo | None = None,
-
value_sharing: bool = False,
-
default: typing.Callable[[cbor2.CBOREncoder, typing.Any], typing.Any] | None = None,
-
canonical: bool = False,
-
date_as_datetime: bool = False,
-
string_referencing: bool = False,
-
indefinite_containers: bool = False,
-
) -> None:
-
CBOREncoder(
-
fp,
-
datetime_as_timestamp=datetime_as_timestamp,
-
timezone=timezone,
-
value_sharing=value_sharing,
-
default=default,
-
canonical=canonical,
-
date_as_datetime=date_as_datetime,
-
string_referencing=string_referencing,
-
indefinite_containers=indefinite_containers,
-
).encode(obj)
+178
src/atpasser/data/decoder.py
···
+
"""
+
JSON decoder for ATProto data model.
+
+
This module provides a JSON decoder that handles ATProto-specific data types,
+
including bytes, CID links, and typed objects.
+
"""
+
+
import base64
+
import json
+
from typing import Any, Callable, Dict, Optional
+
from cid import CIDv0, CIDv1, make_cid
+
+
+
class JsonDecoder(json.JSONDecoder):
+
"""A JSON decoder that supports ATProto data types.
+
+
This decoder extends the standard JSON decoder to handle ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
+
Attributes:
+
type_hook_registry: Registry for type-specific hooks.
+
encoding: The encoding to use for string deserialization.
+
"""
+
+
def __init__(
+
self,
+
*,
+
object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None,
+
type_hook_registry: Optional[Any] = None,
+
type_processor_registry: Optional[Any] = None,
+
encoding: str = "utf-8",
+
**kwargs: Any
+
) -> None:
+
"""Initialize the JSON decoder.
+
+
Args:
+
object_hook: Optional function to call with each decoded object.
+
type_hook_registry: Registry for type-specific hooks.
+
type_processor_registry: Registry for type-specific processors.
+
encoding: The encoding to use for string deserialization.
+
**kwargs: Additional keyword arguments to pass to the parent class.
+
"""
+
# Use the type processor registry if provided, otherwise use the type hook registry
+
if type_processor_registry is not None:
+
type_hook_registry = type_processor_registry.to_hook_registry()
+
elif type_hook_registry is None:
+
from .hooks import get_global_registry
+
type_hook_registry = get_global_registry()
+
+
# Create a combined object hook that calls both the custom hook and our hook
+
combined_hook = self._create_combined_hook(object_hook, type_hook_registry)
+
+
super().__init__(object_hook=combined_hook, **kwargs)
+
self.type_hook_registry = type_hook_registry
+
self.type_processor_registry = type_processor_registry
+
self.encoding = encoding
+
+
def _create_combined_hook(
+
self,
+
custom_hook: Optional[Callable[[Dict[str, Any]], Any]],
+
type_hook_registry: Optional[Any]
+
) -> Callable[[Dict[str, Any]], Any]:
+
"""Create a combined object hook function.
+
+
Args:
+
custom_hook: Optional custom object hook function.
+
type_hook_registry: Registry for type-specific hooks.
+
+
Returns:
+
A combined object hook function.
+
"""
+
def combined_hook(obj: Dict[str, Any]) -> Any:
+
# First, apply our ATProto-specific decoding
+
decoded_obj = self._atproto_object_hook(obj)
+
+
# Then, apply the custom hook if provided
+
if custom_hook is not None:
+
decoded_obj = custom_hook(decoded_obj)
+
+
return decoded_obj
+
+
return combined_hook
+
+
def _atproto_object_hook(self, obj: Dict[str, Any]) -> Any:
+
"""Handle ATProto-specific object decoding.
+
+
Args:
+
obj: The object to decode.
+
+
Returns:
+
The decoded object.
+
"""
+
# Handle $bytes key (RFC-4648 base64 decoding)
+
if "$bytes" in obj:
+
if len(obj) != 1:
+
# If there are other keys, this is invalid
+
raise ValueError(f"Invalid $bytes object: {obj}")
+
return base64.b64decode(obj["$bytes"].encode(self.encoding))
+
+
# Handle $link key (CID parsing)
+
elif "$link" in obj:
+
if len(obj) != 1:
+
# If there are other keys, this is invalid
+
raise ValueError(f"Invalid $link object: {obj}")
+
return make_cid(obj["$link"])
+
+
# Handle $type key (typed objects)
+
elif "$type" in obj:
+
type_value = obj["$type"]
+
remaining_obj = {k: v for k, v in obj.items() if k != "$type"}
+
+
# Check if there's a registered type handler
+
if self.type_hook_registry is not None:
+
handler = self.type_hook_registry.get_handler(type_value)
+
if handler is not None:
+
return handler(remaining_obj)
+
+
# If no handler is registered, return a typed object
+
return TypedObject(type_value, remaining_obj)
+
+
# Handle nested objects recursively
+
elif isinstance(obj, dict):
+
return {k: self._atproto_object_hook(v) if isinstance(v, dict) else v
+
for k, v in obj.items()}
+
+
return obj
+
+
+
class TypedObject:
+
"""A typed object in the ATProto data model.
+
+
This class represents an object with a $type field in the ATProto data model.
+
+
Attributes:
+
type: The type of the object.
+
data: The data associated with the object.
+
"""
+
+
def __init__(self, type_name: str, data: Dict[str, Any]) -> None:
+
"""Initialize a typed object.
+
+
Args:
+
type_name: The type of the object.
+
data: The data associated with the object.
+
"""
+
self.type_name = type_name
+
self.data = data
+
+
def __repr__(self) -> str:
+
"""Return a string representation of the typed object.
+
+
Returns:
+
A string representation of the typed object.
+
"""
+
return f"TypedObject(type_name={self.type_name!r}, data={self.data!r})"
+
+
def __eq__(self, other: Any) -> bool:
+
"""Check if two typed objects are equal.
+
+
Args:
+
other: The object to compare with.
+
+
Returns:
+
True if the objects are equal, False otherwise.
+
"""
+
if not isinstance(other, TypedObject):
+
return False
+
return self.type_name == other.type_name and self.data == other.data
+
+
def __atproto_json_encode__(self) -> Dict[str, Any]:
+
"""Encode the typed object to a JSON-serializable format.
+
+
Returns:
+
A JSON-serializable representation of the typed object.
+
"""
+
result = {"$type": self.type_name}
+
result.update(self.data)
+
return result
+82
src/atpasser/data/encoder.py
···
+
"""
+
JSON encoder for ATProto data model.
+
+
This module provides a JSON encoder that handles ATProto-specific data types,
+
including bytes, CID links, and typed objects.
+
"""
+
+
import base64
+
import json
+
from typing import Any, Optional
+
from cid import CIDv0, CIDv1
+
+
+
class JsonEncoder(json.JSONEncoder):
+
"""A JSON encoder that supports ATProto data types.
+
+
This encoder extends the standard JSON encoder to handle ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
+
Attributes:
+
encoding (str): The encoding to use for string serialization.
+
type_processor_registry: Registry for type-specific processors.
+
"""
+
+
def __init__(
+
self,
+
*,
+
encoding: str = "utf-8",
+
type_processor_registry: Optional[Any] = None,
+
**kwargs: Any
+
) -> None:
+
"""Initialize the JSON encoder.
+
+
Args:
+
encoding: The encoding to use for string serialization.
+
type_processor_registry: Registry for type-specific processors.
+
**kwargs: Additional keyword arguments to pass to the parent class.
+
"""
+
super().__init__(**kwargs)
+
self.encoding = encoding
+
self.type_processor_registry = type_processor_registry
+
+
def default(self, o: Any) -> Any:
+
"""Convert an object to a serializable format.
+
+
Args:
+
o: The object to serialize.
+
+
Returns:
+
A serializable representation of the object.
+
+
Raises:
+
TypeError: If the object is not serializable.
+
"""
+
if isinstance(o, bytes):
+
# Handle bytes using RFC-4648 base64 encoding
+
return {"$bytes": base64.b64encode(o).decode(self.encoding)}
+
elif isinstance(o, (CIDv0, CIDv1)):
+
# Handle CID objects
+
return {"$link": str(o)}
+
elif hasattr(o, "__atproto_json_encode__"):
+
# Handle objects with custom ATProto encoding
+
return o.__atproto_json_encode__()
+
elif self.type_processor_registry is not None:
+
# Try to find a type processor for this object
+
obj_type_name = type(o).__name__
+
encoder = self.type_processor_registry.get_encoder(obj_type_name)
+
if encoder is not None:
+
result = encoder(o)
+
# Add $type field if not already present
+
if isinstance(result, dict) and "$type" not in result:
+
result["$type"] = obj_type_name
+
return result
+
elif isinstance(o, dict):
+
# Handle dictionaries recursively
+
return {k: self.default(v) for k, v in o.items()}
+
elif isinstance(o, (list, tuple)):
+
# Handle lists and tuples recursively
+
return [self.default(item) for item in o]
+
else:
+
# Use the parent class for other types
+
return super().default(o)
+222
src/atpasser/data/hooks.py
···
+
"""
+
Type hook system for ATProto JSON decoder.
+
+
This module provides a decorator-based system for registering custom type handlers
+
for objects with $type keys in the ATProto data model.
+
"""
+
+
import functools
+
from typing import Any, Callable, Dict, Optional, TypeVar, Union
+
+
# Type variable for the decorated function
+
F = TypeVar('F', bound=Callable[..., Any])
+
+
+
class TypeHookRegistry:
+
"""Registry for type-specific hooks in the ATProto JSON decoder.
+
+
This class maintains a registry of type-specific hooks that can be used
+
to customize the decoding of objects with $type keys in the ATProto data model.
+
+
Attributes:
+
_handlers: Dictionary mapping type names to handler functions.
+
"""
+
+
def __init__(self) -> None:
+
"""Initialize the type hook registry."""
+
self._handlers: Dict[str, Callable[[Dict[str, Any]], Any]] = {}
+
+
def register(self, type_name: str) -> Callable[[F], F]:
+
"""Register a type handler function.
+
+
This method can be used as a decorator to register a function as a handler
+
for a specific type.
+
+
Args:
+
type_name: The name of the type to handle.
+
+
Returns:
+
A decorator function that registers the decorated function as a handler.
+
+
Example:
+
>>> registry = TypeHookRegistry()
+
>>>
+
>>> @registry.register("app.bsky.feed.post")
+
... def handle_post(data: Dict[str, Any]) -> Any:
+
... return Post(**data)
+
"""
+
def decorator(func: F) -> F:
+
self._handlers[type_name] = func
+
return func
+
+
return decorator
+
+
def register_handler(self, type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None:
+
"""Register a type handler function directly.
+
+
Args:
+
type_name: The name of the type to handle.
+
handler: The function to call when decoding objects of this type.
+
+
Example:
+
>>> registry = TypeHookRegistry()
+
>>>
+
>>> def handle_post(data: Dict[str, Any]) -> Any:
+
... return Post(**data)
+
>>>
+
>>> registry.register_handler("app.bsky.feed.post", handle_post)
+
"""
+
self._handlers[type_name] = handler
+
+
def unregister(self, type_name: str) -> None:
+
"""Unregister a type handler function.
+
+
Args:
+
type_name: The name of the type to unregister.
+
"""
+
if type_name in self._handlers:
+
del self._handlers[type_name]
+
+
def get_handler(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
+
"""Get the handler function for a specific type.
+
+
Args:
+
type_name: The name of the type to get the handler for.
+
+
Returns:
+
The handler function for the specified type, or None if no handler
+
is registered.
+
"""
+
return self._handlers.get(type_name)
+
+
def has_handler(self, type_name: str) -> bool:
+
"""Check if a handler is registered for a specific type.
+
+
Args:
+
type_name: The name of the type to check.
+
+
Returns:
+
True if a handler is registered for the specified type, False otherwise.
+
"""
+
return type_name in self._handlers
+
+
def clear(self) -> None:
+
"""Clear all registered handlers."""
+
self._handlers.clear()
+
+
def get_registered_types(self) -> set:
+
"""Get the set of all registered type names.
+
+
Returns:
+
A set of all registered type names.
+
"""
+
return set(self._handlers.keys())
+
+
+
# Global registry instance
+
_global_registry = TypeHookRegistry()
+
+
+
def type_handler(type_name: str) -> Callable[[F], F]:
+
"""Register a global type handler function.
+
+
This decorator registers a function as a global handler for a specific type
+
in the ATProto data model.
+
+
Args:
+
type_name: The name of the type to handle.
+
+
Returns:
+
A decorator function that registers the decorated function as a handler.
+
+
Example:
+
>>> @type_handler("app.bsky.feed.post")
+
... def handle_post(data: Dict[str, Any]) -> Any:
+
... return Post(**data)
+
"""
+
return _global_registry.register(type_name)
+
+
+
def get_global_registry() -> TypeHookRegistry:
+
"""Get the global type hook registry.
+
+
Returns:
+
The global TypeHookRegistry instance.
+
"""
+
return _global_registry
+
+
+
def register_type_handler(type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None:
+
"""Register a global type handler function directly.
+
+
Args:
+
type_name: The name of the type to handle.
+
handler: The function to call when decoding objects of this type.
+
+
Example:
+
>>> def handle_post(data: Dict[str, Any]) -> Any:
+
... return Post(**data)
+
>>>
+
>>> register_type_handler("app.bsky.feed.post", handle_post)
+
"""
+
_global_registry.register_handler(type_name, handler)
+
+
+
def unregister_type_handler(type_name: str) -> None:
+
"""Unregister a global type handler function.
+
+
Args:
+
type_name: The name of the type to unregister.
+
"""
+
_global_registry.unregister(type_name)
+
+
+
def get_type_handler(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
+
"""Get the global handler function for a specific type.
+
+
Args:
+
type_name: The name of the type to get the handler for.
+
+
Returns:
+
The handler function for the specified type, or None if no handler
+
is registered.
+
"""
+
return _global_registry.get_handler(type_name)
+
+
+
def has_type_handler(type_name: str) -> bool:
+
"""Check if a global handler is registered for a specific type.
+
+
Args:
+
type_name: The name of the type to check.
+
+
Returns:
+
True if a handler is registered for the specified type, False otherwise.
+
"""
+
return _global_registry.has_handler(type_name)
+
+
+
def clear_type_handlers() -> None:
+
"""Clear all globally registered handlers."""
+
_global_registry.clear()
+
+
+
def get_registered_types() -> set:
+
"""Get the set of all globally registered type names.
+
+
Returns:
+
A set of all registered type names.
+
"""
+
return _global_registry.get_registered_types()
+
+
+
def create_registry() -> TypeHookRegistry:
+
"""Create a new type hook registry.
+
+
This function creates a new, independent registry that can be used
+
instead of the global registry.
+
+
Returns:
+
A new TypeHookRegistry instance.
+
"""
+
return TypeHookRegistry()
+504
src/atpasser/data/types.py
···
+
"""
+
Type processor system for ATProto JSON decoder.
+
+
This module provides an advanced type processor system that allows users to
+
register custom type converters for objects with $type keys in the ATProto data model.
+
"""
+
+
import inspect
+
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union
+
from .hooks import TypeHookRegistry
+
+
# Type variable for the decorated class
+
T = TypeVar('T')
+
+
+
class TypeProcessor:
+
"""A type processor for ATProto JSON objects.
+
+
This class represents a processor for a specific type in the ATProto data model.
+
It contains information about how to convert JSON data to Python objects and
+
vice versa.
+
+
Attributes:
+
type_name: The name of the type this processor handles.
+
decoder: The function to decode JSON data to a Python object.
+
encoder: The function to encode a Python object to JSON data.
+
priority: The priority of this processor (higher values = higher priority).
+
"""
+
+
def __init__(
+
self,
+
type_name: str,
+
decoder: Optional[Callable[[Dict[str, Any]], Any]] = None,
+
encoder: Optional[Callable[[Any], Dict[str, Any]]] = None,
+
priority: int = 0
+
) -> None:
+
"""Initialize a type processor.
+
+
Args:
+
type_name: The name of the type this processor handles.
+
decoder: The function to decode JSON data to a Python object.
+
encoder: The function to encode a Python object to JSON data.
+
priority: The priority of this processor (higher values = higher priority).
+
"""
+
self.type_name = type_name
+
self.decoder = decoder
+
self.encoder = encoder
+
self.priority = priority
+
+
def decode(self, data: Dict[str, Any]) -> Any:
+
"""Decode JSON data to a Python object.
+
+
Args:
+
data: The JSON data to decode.
+
+
Returns:
+
The decoded Python object.
+
+
Raises:
+
ValueError: If no decoder is registered.
+
"""
+
if self.decoder is None:
+
raise ValueError(f"No decoder registered for type {self.type_name}")
+
return self.decoder(data)
+
+
def encode(self, obj: Any) -> Dict[str, Any]:
+
"""Encode a Python object to JSON data.
+
+
Args:
+
obj: The Python object to encode.
+
+
Returns:
+
The encoded JSON data.
+
+
Raises:
+
ValueError: If no encoder is registered.
+
"""
+
if self.encoder is None:
+
raise ValueError(f"No encoder registered for type {self.type_name}")
+
return self.encoder(obj)
+
+
+
class TypeProcessorRegistry:
+
"""Registry for type processors in the ATProto JSON decoder.
+
+
This class maintains a registry of type processors that can be used
+
to customize the encoding and decoding of objects with $type keys in
+
the ATProto data model.
+
+
Attributes:
+
_processors: Dictionary mapping type names to processor lists.
+
"""
+
+
def __init__(self) -> None:
+
"""Initialize the type processor registry."""
+
self._processors: Dict[str, List[TypeProcessor]] = {}
+
+
def register_processor(self, processor: TypeProcessor) -> None:
+
"""Register a type processor.
+
+
Args:
+
processor: The type processor to register.
+
"""
+
if processor.type_name not in self._processors:
+
self._processors[processor.type_name] = []
+
+
self._processors[processor.type_name].append(processor)
+
# Sort processors by priority (descending)
+
self._processors[processor.type_name].sort(key=lambda p: p.priority, reverse=True)
+
+
def register(
+
self,
+
type_name: str,
+
priority: int = 0
+
) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
+
"""Register a type decoder function.
+
+
This method can be used as a decorator to register a function as a decoder
+
for a specific type.
+
+
Args:
+
type_name: The name of the type to handle.
+
priority: The priority of this processor (higher values = higher priority).
+
+
Returns:
+
A decorator function that registers the decorated function as a decoder.
+
+
Example:
+
>>> registry = TypeProcessorRegistry()
+
>>>
+
>>> @registry.register("app.bsky.feed.post", priority=10)
+
... def decode_post(data: Dict[str, Any]) -> Any:
+
... return Post(**data)
+
"""
+
def decorator(func: Callable[[Dict[str, Any]], Any]) -> Callable[[Dict[str, Any]], Any]:
+
processor = TypeProcessor(type_name, decoder=func, priority=priority)
+
self.register_processor(processor)
+
return func
+
+
return decorator
+
+
def register_encoder(
+
self,
+
type_name: str,
+
priority: int = 0
+
) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
+
"""Register a type encoder function.
+
+
This method can be used as a decorator to register a function as an encoder
+
for a specific type.
+
+
Args:
+
type_name: The name of the type to handle.
+
priority: The priority of this processor (higher values = higher priority).
+
+
Returns:
+
A decorator function that registers the decorated function as an encoder.
+
+
Example:
+
>>> registry = TypeProcessorRegistry()
+
>>>
+
>>> @registry.register_encoder("app.bsky.feed.post", priority=10)
+
... def encode_post(post: Post) -> Dict[str, Any]:
+
... return {"text": post.text, "createdAt": post.created_at}
+
"""
+
def decorator(func: Callable[[Any], Dict[str, Any]]) -> Callable[[Any], Dict[str, Any]]:
+
# Check if a processor for this type already exists
+
if type_name in self._processors:
+
for processor in self._processors[type_name]:
+
if processor.decoder is not None:
+
# Update the existing processor with the encoder
+
processor.encoder = func
+
break
+
else:
+
# No decoder found, create a new processor
+
processor = TypeProcessor(type_name, encoder=func, priority=priority)
+
self.register_processor(processor)
+
else:
+
# No processor exists, create a new one
+
processor = TypeProcessor(type_name, encoder=func, priority=priority)
+
self.register_processor(processor)
+
+
return func
+
+
return decorator
+
+
def register_class(
+
self,
+
type_name: str,
+
priority: int = 0
+
) -> Callable[[Type[T]], Type[T]]:
+
"""Register a class for both encoding and decoding.
+
+
This method can be used as a decorator to register a class for both
+
encoding and decoding of a specific type.
+
+
The class must have a class method `from_json` that takes a dictionary
+
and returns an instance of the class, and an instance method `to_json`
+
that returns a dictionary.
+
+
Args:
+
type_name: The name of the type to handle.
+
priority: The priority of this processor (higher values = higher priority).
+
+
Returns:
+
A decorator function that registers the decorated class.
+
+
Example:
+
>>> registry = TypeProcessorRegistry()
+
>>>
+
>>> @registry.register_class("app.bsky.feed.post", priority=10)
+
... class Post:
+
... def __init__(self, text: str, created_at: str) -> None:
+
... self.text = text
+
... self.created_at = created_at
+
...
+
... @classmethod
+
... def from_json(cls, data: Dict[str, Any]) -> "Post":
+
... return cls(data["text"], data["createdAt"])
+
...
+
... def to_json(self) -> Dict[str, Any]:
+
... return {"text": self.text, "createdAt": self.created_at}
+
"""
+
def decorator(cls: Type[T]) -> Type[T]:
+
# Create decoder from class method
+
if hasattr(cls, "from_json"):
+
decoder = lambda data: getattr(cls, "from_json")(data)
+
else:
+
# Try to create a decoder from the constructor
+
init_signature = inspect.signature(cls.__init__)
+
if init_signature.parameters:
+
# Create a decoder that passes the data as keyword arguments
+
decoder = lambda data: cls(**data)
+
else:
+
raise ValueError(f"Class {cls.__name__} has no from_json method or compatible __init__")
+
+
# Create encoder from instance method
+
if hasattr(cls, "to_json"):
+
encoder = lambda obj: obj.to_json()
+
else:
+
raise ValueError(f"Class {cls.__name__} has no to_json method")
+
+
# Register the processor
+
processor = TypeProcessor(type_name, decoder=decoder, encoder=encoder, priority=priority)
+
self.register_processor(processor)
+
+
return cls
+
+
return decorator
+
+
def unregister(self, type_name: str, priority: Optional[int] = None) -> None:
+
"""Unregister type processors.
+
+
Args:
+
type_name: The name of the type to unregister.
+
priority: If specified, only unregister processors with this priority.
+
"""
+
if type_name in self._processors:
+
if priority is not None:
+
# Remove processors with the specified priority
+
self._processors[type_name] = [
+
p for p in self._processors[type_name] if p.priority != priority
+
]
+
else:
+
# Remove all processors for this type
+
del self._processors[type_name]
+
+
def get_decoder(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
+
"""Get the decoder function for a specific type.
+
+
Args:
+
type_name: The name of the type to get the decoder for.
+
+
Returns:
+
The decoder function for the specified type, or None if no decoder
+
is registered.
+
"""
+
if type_name in self._processors and self._processors[type_name]:
+
# Return the decoder of the highest priority processor
+
return self._processors[type_name][0].decoder
+
return None
+
+
def get_encoder(self, type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
+
"""Get the encoder function for a specific type.
+
+
Args:
+
type_name: The name of the type to get the encoder for.
+
+
Returns:
+
The encoder function for the specified type, or None if no encoder
+
is registered.
+
"""
+
if type_name in self._processors and self._processors[type_name]:
+
# Return the encoder of the highest priority processor
+
return self._processors[type_name][0].encoder
+
return None
+
+
def has_processor(self, type_name: str) -> bool:
+
"""Check if a processor is registered for a specific type.
+
+
Args:
+
type_name: The name of the type to check.
+
+
Returns:
+
True if a processor is registered for the specified type, False otherwise.
+
"""
+
return type_name in self._processors and bool(self._processors[type_name])
+
+
def clear(self) -> None:
+
"""Clear all registered processors."""
+
self._processors.clear()
+
+
def get_registered_types(self) -> set:
+
"""Get the set of all registered type names.
+
+
Returns:
+
A set of all registered type names.
+
"""
+
return set(self._processors.keys())
+
+
def to_hook_registry(self) -> TypeHookRegistry:
+
"""Convert this processor registry to a hook registry.
+
+
This method creates a TypeHookRegistry that uses the decoders from
+
this processor registry.
+
+
Returns:
+
A TypeHookRegistry with the same decoders as this processor registry.
+
"""
+
hook_registry = TypeHookRegistry()
+
+
for type_name, processors in self._processors.items():
+
if processors and processors[0].decoder is not None:
+
hook_registry.register_handler(type_name, processors[0].decoder)
+
+
return hook_registry
+
+
+
# Global registry instance
+
_global_processor_registry = TypeProcessorRegistry()
+
+
+
def register_type(
+
type_name: str,
+
priority: int = 0
+
) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
+
"""Register a global type decoder function.
+
+
This decorator registers a function as a global decoder for a specific type
+
in the ATProto data model.
+
+
Args:
+
type_name: The name of the type to handle.
+
priority: The priority of this processor (higher values = higher priority).
+
+
Returns:
+
A decorator function that registers the decorated function as a decoder.
+
+
Example:
+
>>> @register_type("app.bsky.feed.post", priority=10)
+
... def decode_post(data: Dict[str, Any]) -> Any:
+
... return Post(**data)
+
"""
+
return _global_processor_registry.register(type_name, priority)
+
+
+
def get_global_processor_registry() -> TypeProcessorRegistry:
+
"""Get the global type processor registry.
+
+
Returns:
+
The global TypeProcessorRegistry instance.
+
"""
+
return _global_processor_registry
+
+
+
def register_type_encoder(
+
type_name: str,
+
priority: int = 0
+
) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
+
"""Register a global type encoder function.
+
+
This decorator registers a function as a global encoder for a specific type
+
in the ATProto data model.
+
+
Args:
+
type_name: The name of the type to handle.
+
priority: The priority of this processor (higher values = higher priority).
+
+
Returns:
+
A decorator function that registers the decorated function as an encoder.
+
+
Example:
+
>>> @register_type_encoder("app.bsky.feed.post", priority=10)
+
... def encode_post(post: Post) -> Dict[str, Any]:
+
... return {"text": post.text, "createdAt": post.created_at}
+
"""
+
return _global_processor_registry.register_encoder(type_name, priority)
+
+
+
def register_type_class(
+
type_name: str,
+
priority: int = 0
+
) -> Callable[[Type[T]], Type[T]]:
+
"""Register a class for both global encoding and decoding.
+
+
This decorator registers a class for both encoding and decoding of a specific type
+
in the ATProto data model.
+
+
Args:
+
type_name: The name of the type to handle.
+
priority: The priority of this processor (higher values = higher priority).
+
+
Returns:
+
A decorator function that registers the decorated class.
+
+
Example:
+
>>> @register_type_class("app.bsky.feed.post", priority=10)
+
... class Post:
+
... def __init__(self, text: str, created_at: str) -> None:
+
... self.text = text
+
... self.created_at = created_at
+
...
+
... @classmethod
+
... def from_json(cls, data: Dict[str, Any]) -> "Post":
+
... return cls(data["text"], data["createdAt"])
+
...
+
... def to_json(self) -> Dict[str, Any]:
+
... return {"text": self.text, "createdAt": self.created_at}
+
"""
+
return _global_processor_registry.register_class(type_name, priority)
+
+
+
def unregister_type(type_name: str, priority: Optional[int] = None) -> None:
+
"""Unregister global type processors.
+
+
Args:
+
type_name: The name of the type to unregister.
+
priority: If specified, only unregister processors with this priority.
+
"""
+
_global_processor_registry.unregister(type_name, priority)
+
+
+
def get_type_decoder(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
+
"""Get the global decoder function for a specific type.
+
+
Args:
+
type_name: The name of the type to get the decoder for.
+
+
Returns:
+
The decoder function for the specified type, or None if no decoder
+
is registered.
+
"""
+
return _global_processor_registry.get_decoder(type_name)
+
+
+
def get_type_encoder(type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
+
"""Get the global encoder function for a specific type.
+
+
Args:
+
type_name: The name of the type to get the encoder for.
+
+
Returns:
+
The encoder function for the specified type, or None if no encoder
+
is registered.
+
"""
+
return _global_processor_registry.get_encoder(type_name)
+
+
+
def has_type_processor(type_name: str) -> bool:
+
"""Check if a global processor is registered for a specific type.
+
+
Args:
+
type_name: The name of the type to check.
+
+
Returns:
+
True if a processor is registered for the specified type, False otherwise.
+
"""
+
return _global_processor_registry.has_processor(type_name)
+
+
+
def clear_type_processors() -> None:
+
"""Clear all globally registered processors."""
+
_global_processor_registry.clear()
+
+
+
def get_registered_types() -> set:
+
"""Get the set of all globally registered type names.
+
+
Returns:
+
A set of all registered type names.
+
"""
+
return _global_processor_registry.get_registered_types()
+
+
+
def create_processor_registry() -> TypeProcessorRegistry:
+
"""Create a new type processor registry.
+
+
This function creates a new, independent registry that can be used
+
instead of the global registry.
+
+
Returns:
+
A new TypeProcessorRegistry instance.
+
"""
+
return TypeProcessorRegistry()
+339
src/atpasser/data/wrapper.py
···
+
"""
+
JSON wrapper functions for ATProto data model.
+
+
This module provides wrapper functions that mirror the standard json module
+
but with support for ATProto-specific data types.
+
"""
+
+
import json
+
import io
+
from typing import Any, Callable, Dict, Optional, TextIO, Union
+
from .encoder import JsonEncoder
+
from .decoder import JsonDecoder
+
from .hooks import TypeHookRegistry
+
from .types import TypeProcessorRegistry
+
+
+
def dump(
+
obj: Any,
+
fp: TextIO,
+
*,
+
skipkeys: bool = False,
+
ensure_ascii: bool = True,
+
check_circular: bool = True,
+
allow_nan: bool = True,
+
cls: Optional[type[JsonEncoder]] = None,
+
indent: Optional[Union[int, str]] = None,
+
separators: Optional[tuple[str, str]] = None,
+
default: Optional[Callable[[Any], Any]] = None,
+
sort_keys: bool = False,
+
encoding: str = "utf-8",
+
type_processor_registry: Optional[TypeProcessorRegistry] = None,
+
**kwargs: Any
+
) -> None:
+
"""Serialize obj as a JSON formatted stream to fp.
+
+
This function is similar to json.dump() but supports ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
+
Args:
+
obj: The object to serialize.
+
fp: A file-like object with a write() method.
+
skipkeys: If True, dict keys that are not basic types (str, int, float,
+
bool, None) will be skipped instead of raising a TypeError.
+
ensure_ascii: If True, the output is guaranteed to have all incoming
+
non-ASCII characters escaped. If False, these characters will be
+
output as-is.
+
check_circular: If True, circular references will be checked and
+
a CircularReferenceError will be raised if one is found.
+
allow_nan: If True, NaN, Infinity, and -Infinity will be encoded as
+
such. This behavior is not JSON specification compliant, but it
+
is consistent with most JavaScript based encoders and decoders.
+
Otherwise, it will raise a ValueError.
+
cls: A custom JSONEncoder subclass. If not specified, JsonEncoder is used.
+
indent: If indent is a non-negative integer or string, then JSON array
+
elements and object members will be pretty-printed with that indent
+
level. An indent level of 0, negative, or "" will only insert newlines.
+
None (the default) selects the most compact representation.
+
separators: If specified, separators should be an (item_separator, key_separator)
+
tuple. The default is (', ', ': ') if indent is None and (',', ': ') otherwise.
+
To get the most compact JSON representation, you should specify (',', ':')
+
to eliminate whitespace.
+
default: If specified, default should be a function that gets called for
+
objects that can't otherwise be serialized. It should return a JSON
+
encodable version of the object or raise a TypeError.
+
sort_keys: If sort_keys is True, then the output of dictionaries will be
+
sorted by key.
+
encoding: The encoding to use for string serialization.
+
type_processor_registry: Registry for type-specific processors.
+
**kwargs: Additional keyword arguments to pass to the JSON encoder.
+
"""
+
if cls is None:
+
cls = JsonEncoder
+
+
# Use the global type processor registry if none is provided
+
if type_processor_registry is None:
+
from .types import get_global_processor_registry
+
type_processor_registry = get_global_processor_registry()
+
+
# Create an encoder instance with the specified encoding and type processor registry
+
encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs)
+
+
# Use the standard json.dump with our custom encoder
+
json.dump(
+
obj,
+
fp,
+
skipkeys=skipkeys,
+
ensure_ascii=ensure_ascii,
+
check_circular=check_circular,
+
allow_nan=allow_nan,
+
cls=cls,
+
indent=indent,
+
separators=separators,
+
default=default,
+
sort_keys=sort_keys,
+
**kwargs
+
)
+
+
+
def dumps(
+
obj: Any,
+
*,
+
skipkeys: bool = False,
+
ensure_ascii: bool = True,
+
check_circular: bool = True,
+
allow_nan: bool = True,
+
cls: Optional[type[JsonEncoder]] = None,
+
indent: Optional[Union[int, str]] = None,
+
separators: Optional[tuple[str, str]] = None,
+
default: Optional[Callable[[Any], Any]] = None,
+
sort_keys: bool = False,
+
encoding: str = "utf-8",
+
type_processor_registry: Optional[TypeProcessorRegistry] = None,
+
**kwargs: Any
+
) -> str:
+
"""Serialize obj to a JSON formatted string.
+
+
This function is similar to json.dumps() but supports ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
+
Args:
+
obj: The object to serialize.
+
skipkeys: If True, dict keys that are not basic types (str, int, float,
+
bool, None) will be skipped instead of raising a TypeError.
+
ensure_ascii: If True, the output is guaranteed to have all incoming
+
non-ASCII characters escaped. If False, these characters will be
+
output as-is.
+
check_circular: If True, circular references will be checked and
+
a CircularReferenceError will be raised if one is found.
+
allow_nan: If True, NaN, Infinity, and -Infinity will be encoded as
+
such. This behavior is not JSON specification compliant, but it
+
is consistent with most JavaScript based encoders and decoders.
+
Otherwise, it will raise a ValueError.
+
cls: A custom JSONEncoder subclass. If not specified, JsonEncoder is used.
+
indent: If indent is a non-negative integer or string, then JSON array
+
elements and object members will be pretty-printed with that indent
+
level. An indent level of 0, negative, or "" will only insert newlines.
+
None (the default) selects the most compact representation.
+
separators: If specified, separators should be an (item_separator, key_separator)
+
tuple. The default is (', ', ': ') if indent is None and (',', ': ') otherwise.
+
To get the most compact JSON representation, you should specify (',', ':')
+
to eliminate whitespace.
+
default: If specified, default should be a function that gets called for
+
objects that can't otherwise be serialized. It should return a JSON
+
encodable version of the object or raise a TypeError.
+
sort_keys: If sort_keys is True, then the output of dictionaries will be
+
sorted by key.
+
encoding: The encoding to use for string serialization.
+
type_processor_registry: Registry for type-specific processors.
+
**kwargs: Additional keyword arguments to pass to the JSON encoder.
+
+
Returns:
+
A JSON formatted string.
+
"""
+
if cls is None:
+
cls = JsonEncoder
+
+
# Create an encoder instance with the specified encoding and type processor registry
+
encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs)
+
+
# Use the standard json.dumps with our custom encoder
+
return json.dumps(
+
obj,
+
skipkeys=skipkeys,
+
ensure_ascii=ensure_ascii,
+
check_circular=check_circular,
+
allow_nan=allow_nan,
+
cls=cls,
+
indent=indent,
+
separators=separators,
+
default=default,
+
sort_keys=sort_keys,
+
**kwargs
+
)
+
+
+
def load(
+
fp: TextIO,
+
*,
+
cls: Optional[type[JsonDecoder]] = None,
+
object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None,
+
parse_float: Optional[Callable[[str], Any]] = None,
+
parse_int: Optional[Callable[[str], Any]] = None,
+
parse_constant: Optional[Callable[[str], Any]] = None,
+
object_pairs_hook: Optional[Callable[[list[tuple[str, Any]]], Any]] = None,
+
type_hook_registry: Optional[TypeHookRegistry] = None,
+
type_processor_registry: Optional[TypeProcessorRegistry] = None,
+
encoding: str = "utf-8",
+
**kwargs: Any
+
) -> Any:
+
"""Deserialize fp (a .read()-supporting text file or binary file containing
+
a JSON document) to a Python object.
+
+
This function is similar to json.load() but supports ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
+
Args:
+
fp: A .read()-supporting text file or binary file containing a JSON document.
+
cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
+
object_hook: Optional function that will be called with the result of
+
every JSON object decoded and its return value will be used in place
+
of the given dict.
+
parse_float: Optional function that will be called with the string of
+
every JSON float to be decoded. By default, this is equivalent to
+
float(num_str). This can be used to use another datatype or parser
+
for JSON floats (e.g. decimal.Decimal).
+
parse_int: Optional function that will be called with the string of
+
every JSON int to be decoded. By default, this is equivalent to
+
int(num_str). This can be used to use another datatype or parser
+
for JSON integers (e.g. float).
+
parse_constant: Optional function that will be called with the string of
+
every JSON constant to be decoded. By default, this is equivalent to
+
constant_mapping[constant_str]. This can be used to use another
+
datatype or parser for JSON constants (e.g. decimal.Decimal).
+
object_pairs_hook: Optional function that will be called with the result
+
of every JSON object decoded with an ordered list of pairs. The return
+
value of object_pairs_hook will be used instead of the dict. This
+
feature can be used to implement custom decoders. If object_hook is
+
also defined, the object_pairs_hook takes priority.
+
type_hook_registry: Registry for type-specific hooks.
+
type_processor_registry: Registry for type-specific processors.
+
encoding: The encoding to use for string deserialization.
+
**kwargs: Additional keyword arguments to pass to the JSON decoder.
+
+
Returns:
+
A Python object.
+
"""
+
if cls is None:
+
cls = JsonDecoder
+
+
# Use the global type hook registry if none is provided
+
if type_hook_registry is None and type_processor_registry is None:
+
from .hooks import get_global_registry
+
type_hook_registry = get_global_registry()
+
elif type_processor_registry is not None:
+
# Convert the type processor registry to a hook registry
+
type_hook_registry = type_processor_registry.to_hook_registry()
+
+
# Create a decoder instance with the specified parameters
+
decoder = cls(
+
object_hook=object_hook,
+
type_hook_registry=type_hook_registry,
+
encoding=encoding,
+
**kwargs
+
)
+
+
# Use the standard json.load with our custom decoder
+
return json.load(
+
fp,
+
cls=cls,
+
object_hook=object_hook,
+
parse_float=parse_float,
+
parse_int=parse_int,
+
parse_constant=parse_constant,
+
object_pairs_hook=object_pairs_hook,
+
**kwargs
+
)
+
+
+
def loads(
+
s: Union[str, bytes],
+
*,
+
cls: Optional[type[JsonDecoder]] = None,
+
object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None,
+
parse_float: Optional[Callable[[str], Any]] = None,
+
parse_int: Optional[Callable[[str], Any]] = None,
+
parse_constant: Optional[Callable[[str], Any]] = None,
+
object_pairs_hook: Optional[Callable[[list[tuple[str, Any]]], Any]] = None,
+
type_hook_registry: Optional[TypeHookRegistry] = None,
+
type_processor_registry: Optional[TypeProcessorRegistry] = None,
+
encoding: str = "utf-8",
+
**kwargs: Any
+
) -> Any:
+
"""Deserialize s (a str, bytes or bytearray instance containing a JSON document)
+
to a Python object.
+
+
This function is similar to json.loads() but supports ATProto-specific
+
data types, including bytes, CID links, and typed objects.
+
+
Args:
+
s: A str, bytes or bytearray instance containing a JSON document.
+
cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
+
object_hook: Optional function that will be called with the result of
+
every JSON object decoded and its return value will be used in place
+
of the given dict.
+
parse_float: Optional function that will be called with the string of
+
every JSON float to be decoded. By default, this is equivalent to
+
float(num_str). This can be used to use another datatype or parser
+
for JSON floats (e.g. decimal.Decimal).
+
parse_int: Optional function that will be called with the string of
+
every JSON int to be decoded. By default, this is equivalent to
+
int(num_str). This can be used to use another datatype or parser
+
for JSON integers (e.g. float).
+
parse_constant: Optional function that will be called with the string of
+
every JSON constant to be decoded. By default, this is equivalent to
+
constant_mapping[constant_str]. This can be used to use another
+
datatype or parser for JSON constants (e.g. decimal.Decimal).
+
object_pairs_hook: Optional function that will be called with the result
+
of every JSON object decoded with an ordered list of pairs. The return
+
value of object_pairs_hook will be used instead of the dict. This
+
feature can be used to implement custom decoders. If object_hook is
+
also defined, the object_pairs_hook takes priority.
+
type_hook_registry: Registry for type-specific hooks.
+
type_processor_registry: Registry for type-specific processors.
+
encoding: The encoding to use for string deserialization.
+
**kwargs: Additional keyword arguments to pass to the JSON decoder.
+
+
Returns:
+
A Python object.
+
"""
+
if cls is None:
+
cls = JsonDecoder
+
+
# Use the global type hook registry if none is provided
+
if type_hook_registry is None and type_processor_registry is None:
+
from .hooks import get_global_registry
+
type_hook_registry = get_global_registry()
+
elif type_processor_registry is not None:
+
# Convert the type processor registry to a hook registry
+
type_hook_registry = type_processor_registry.to_hook_registry()
+
+
# Create a decoder instance with the specified parameters
+
decoder = cls(
+
object_hook=object_hook,
+
type_hook_registry=type_hook_registry,
+
encoding=encoding,
+
**kwargs
+
)
+
+
# Use the standard json.loads with our custom decoder
+
return json.loads(
+
s,
+
cls=cls,
+
object_hook=object_hook,
+
parse_float=parse_float,
+
parse_int=parse_int,
+
parse_constant=parse_constant,
+
object_pairs_hook=object_pairs_hook,
+
**kwargs
+
)
-4
tests/__init__.py
···
-
if __name__ != "__main__":
-
raise Exception("name != main")
-
-
import _strings
-179
tests/_strings.py
···
-
from atpasser import did, handle, nsid, rKey, uri
-
-
-
testStrings, testMethods = {}, {}
-
-
-
testStrings[
-
"did"
-
-
] = """did:plc:z72i7hdynmk6r22z27h6tvur
-
-
did:web:blueskyweb.xyz
-
-
did:method:val:two
-
-
did:m:v
-
-
did:method::::val
-
-
did:method:-:_:.
-
-
did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N
-
-
did:METHOD:val
-
-
did:m123:val
-
-
DID:method:val
-
did:method:
-
-
did:method:val/two
-
-
did:method:val?two
-
-
did:method:val#two"""
-
-
testMethods["did"] = did.DID
-
-
-
testStrings[
-
"handle"
-
-
] = """jay.bsky.social
-
-
8.cn
-
-
name.t--t
-
-
XX.LCS.MIT.EDU
-
a.co
-
-
xn--notarealidn.com
-
-
xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s
-
-
xn--ls8h.test
-
example.t
-
-
jo@hn.test
-
-
💩.tes
-
t
-
john..test
-
-
xn--bcher-.tld
-
-
john.0
-
-
cn.8
-
-
www.masełkowski.pl.com
-
-
org
-
-
name.org.
-
-
2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
-
laptop.local
-
-
blah.arpa"""
-
-
testMethods["handle"] = handle.Handle
-
-
-
testStrings[
-
"nsid"
-
-
] = """com.example.fooBar
-
-
net.users.bob.ping
-
-
a-0.b-1.c
-
-
a.b.c
-
-
com.example.fooBarV2
-
-
cn.8.lex.stuff
-
-
com.exa💩ple.thin
-
com.example
-
-
com.example.3"""
-
-
testMethods["nsid"] = nsid.NSID
-
-
-
testStrings[
-
-
"rkey"
-
-
] = """3jui7kd54zh2y
-
self
-
example.com
-
-
~1.2-3_
-
-
dHJ1ZQ
-
pre:fix
-
-
_
-
-
alpha/beta
-
.
-
..
-
-
#extra
-
-
@handle
-
-
any space
-
-
any+space
-
-
number[3]
-
-
number(3)
-
-
"quote"
-
-
dHJ1ZQ=="""
-
-
testMethods["rkey"] = rKey.RKey
-
-
-
testStrings[
-
"uri"
-
-
] = """at://foo.com/com.example.foo/123
-
-
at://foo.com/example/123
-
-
at://computer
-
-
at://example.com:3000
-
-
at://foo.com/
-
-
at://user:pass@foo.com"""
-
-
testMethods["uri"] = uri.URI
-
-
-
for item in testMethods:
-
-
print(f"START TEST {item}")
-
-
for value in testStrings[item].splitlines():
-
-
print(f"Value: {value}")
-
-
try:
-
-
print(f"str(): {str(testMethods[item](value))}")
-
-
except Exception as e:
-
-
print(f"× {e}")
-