Revert "rebuild data model"

This reverts commit ca8fad4e6f2bbf867daf5eda59bdbe694a242399.

+1 -91
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 = "959bc6b01857f65b67d47c12c46abc7af95aecf9913baf195392232500f2253a"
+
content-hash = "5f4e5fd166bf6b2010ec5acaf545a0cfe376dcc5437530f3add4b58f10ce439f"
-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.*"]
+1 -49
src/atpasser/data/__init__.py
···
-
"""
-
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",
-
]
+
from ._wrapper import *
+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)
-182
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)
-227
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()
-510
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()
-346
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 -1
tests/__init__.py
···
-
"""Test package for atpasser."""
+
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}")
+