Compare changes

Choose any two refs to compare.

+3
src/atpasser/uri/__init__.py
···
"JSONPath parsing failed",
f"Failed to parse JSONPath fragment '{fragment}': {str(e)}",
)
if query != None:
try:
···
"JSONPath parsing failed",
f"Failed to parse JSONPath fragment '{fragment}': {str(e)}",
)
+
else:
+
self.fragment = None
+
self.fragmentAsText = None
if query != None:
try:
+1 -1
src/atpasser/uri/identifier.py
···
"Handle's top-level domain cannot start with a digit",
)
-
self.handle = handle
def __str__(self) -> str:
"""Convert the Handle object to its string representation.
···
"Handle's top-level domain cannot start with a digit",
)
+
self.handle = handle.lower()
def __str__(self) -> str:
"""Convert the Handle object to its string representation.
-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()
···
-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,
-
)
···
+6 -1
.gitignore
···
__marimo__/
# Streamlit
-
.streamlit/secrets.toml
···
__marimo__/
# Streamlit
+
.streamlit/secrets.toml
+
+
#####
+
+
# Added by DWN - temp dir
+
tmp/
+10 -2
README.md
···
-
# ATPasser โ•ฌ<!
A simple library for the [Authenticated Transfer Protocol](https://atproto.com/specs/atp) (AT Protocol or atproto for short).
···
---
-
[See the roadmap](docs/roadmap.md)
---
···
+
# ATPasser!
A simple library for the [Authenticated Transfer Protocol](https://atproto.com/specs/atp) (AT Protocol or atproto for short).
···
---
+
## Other ATProto libraries
+
+
[There's an ATProto SDK already (and used by lots of projects) by MarshalX,](https://github.com/MarshalX/atproto) and why do this exists?
+
+
The first reason is that I'm recovering the now-closed [Tietiequan](https://tangled.org/@dwn.dwnfonts.cc/bluesky-circle) app and found that some API has changed so I have to rewrite it via vanilla JS.
+
+
The second reason is that I'm a newbie in ATProto, wanting to know how ATProto is, and how this can be represented in Python.
+
+
The architecture will be small, only containing the data model and the client.
---
-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)
···
-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}")
-
···
+174 -1
poetry.lock
···
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "base58"
version = "1.0.3"
···
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
]
[[package]]
name = "pyld"
version = "2.0.4"
···
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]]
name = "urllib3"
version = "2.5.0"
···
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
-
content-hash = "5f4e5fd166bf6b2010ec5acaf545a0cfe376dcc5437530f3add4b58f10ce439f"
···
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
+
[[package]]
+
name = "annotated-types"
+
version = "0.7.0"
+
description = "Reusable constraint types to use with typing.Annotated"
+
optional = false
+
python-versions = ">=3.8"
+
groups = ["main"]
+
files = [
+
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+
]
+
[[package]]
name = "base58"
version = "1.0.3"
···
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
]
+
[[package]]
+
name = "pydantic"
+
version = "2.11.9"
+
description = "Data validation using Python type hints"
+
optional = false
+
python-versions = ">=3.9"
+
groups = ["main"]
+
files = [
+
{file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"},
+
{file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"},
+
]
+
+
[package.dependencies]
+
annotated-types = ">=0.6.0"
+
pydantic-core = "2.33.2"
+
typing-extensions = ">=4.12.2"
+
typing-inspection = ">=0.4.0"
+
+
[package.extras]
+
email = ["email-validator (>=2.0.0)"]
+
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
+
+
[[package]]
+
name = "pydantic-core"
+
version = "2.33.2"
+
description = "Core functionality for Pydantic validation and serialization"
+
optional = false
+
python-versions = ">=3.9"
+
groups = ["main"]
+
files = [
+
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
+
{file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
+
{file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
+
{file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
+
{file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
+
{file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
+
{file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
+
{file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
+
{file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
+
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
+
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
+
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
+
{file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
+
]
+
+
[package.dependencies]
+
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
[[package]]
name = "pyld"
version = "2.0.4"
···
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
+
[[package]]
+
name = "typing-extensions"
+
version = "4.15.0"
+
description = "Backported and Experimental Type Hints for Python 3.9+"
+
optional = false
+
python-versions = ">=3.9"
+
groups = ["main"]
+
files = [
+
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+
]
+
+
[[package]]
+
name = "typing-inspection"
+
version = "0.4.1"
+
description = "Runtime typing introspection tools"
+
optional = false
+
python-versions = ">=3.9"
+
groups = ["main"]
+
files = [
+
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
+
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
+
]
+
+
[package.dependencies]
+
typing-extensions = ">=4.12.0"
+
[[package]]
name = "urllib3"
version = "2.5.0"
···
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
+
content-hash = "4919ab150fee9e4e358e57bada62225cb2d92c52509b26169db269691b86cefe"
+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
]
license = "MIT OR Apache-2.0"
license-files = ["LICEN[CS]E.*"]
···
"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
+
"pydantic (>=2.11.9,<3.0.0)",
]
license = "MIT OR Apache-2.0"
license-files = ["LICEN[CS]E.*"]
+205
ARCHITECTURE_OVERVIEW.md
···
···
+
# ATProto Data ๅ’Œ Lexicon ๆจกๅ—ๆžถๆž„ๆ€ป่งˆ
+
+
## ้กน็›ฎๆฆ‚่ฟฐ
+
+
ๆœฌ้กน็›ฎไธบ ATProto (Authenticated Transfer Protocol) ๆไพ› Python ๅฎž็Žฐ๏ผŒไธ“ๆณจไบŽๆ•ฐๆฎๆจกๅž‹ๅ’Œ Lexicon ๅฎšไน‰็š„ๅค„็†ใ€‚ๅŸบไบŽ็Žฐๆœ‰็š„ URI ๆจกๅ—ๆžถๆž„ๆจกๅผ๏ผŒๆไพ›็ฑปๅž‹ๅฎ‰ๅ…จ็š„ๆ•ฐๆฎ้ชŒ่ฏใ€ๅบๅˆ—ๅŒ–ๅ’Œ Lexicon ่งฃๆžๅŠŸ่ƒฝใ€‚
+
+
## ๆ•ดไฝ“ๆžถๆž„่ฎพ่ฎก
+
+
### 1. ็ณป็ปŸๆžถๆž„ๅ›พ
+
+
```mermaid
+
graph TB
+
subgraph "ATProto ๆ ธๅฟƒๆจกๅ—"
+
URI[URI ๆจกๅ—]
+
Data[Data ๆจกๅ—]
+
Lexicon[Lexicon ๆจกๅ—]
+
end
+
+
subgraph "ๅค–้ƒจไพ่ต–"
+
Pydantic[Pydantic]
+
CBOR[cbor2]
+
CID[py-cid]
+
end
+
+
subgraph "ๆ•ฐๆฎๆต"
+
LexiconJSON[Lexicon JSON ๆ–‡ไปถ]
+
RawData[ๅŽŸๅง‹ๆ•ฐๆฎ]
+
end
+
+
LexiconJSON --> Lexicon
+
Lexicon --> Data
+
RawData --> Data
+
Data --> Serialized[ๅบๅˆ—ๅŒ–ๆ•ฐๆฎ]
+
+
URI --> Data
+
URI --> Lexicon
+
+
Pydantic --> Data
+
Pydantic --> Lexicon
+
CBOR --> Data
+
CID --> Data
+
```
+
+
### 2. ๆจกๅ—่Œ่ดฃๅˆ’ๅˆ†
+
+
#### 2.1 Data ๆจกๅ— (`src/atpasser/data`)
+
- **ๆ•ฐๆฎๅบๅˆ—ๅŒ–**: JSON ๅ’Œ DAG-CBOR ๆ ผๅผ็š„ๅบๅˆ—ๅŒ–/ๅๅบๅˆ—ๅŒ–
+
- **ๆ•ฐๆฎ้ชŒ่ฏ**: ็ฑปๅž‹้ชŒ่ฏใ€ๆ ผๅผ้ชŒ่ฏใ€็บฆๆŸ้ชŒ่ฏ
+
- **็‰นๆฎŠ็ฑปๅž‹ๅค„็†**: CID ้“พๆŽฅใ€Blob ๅผ•็”จใ€ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ็ญ‰
+
- **้”™่ฏฏๅค„็†**: ่ฏฆ็ป†็š„้ชŒ่ฏ้”™่ฏฏๅ’Œๅบๅˆ—ๅŒ–้”™่ฏฏ
+
+
#### 2.2 Lexicon ๆจกๅ— (`src/atpasser/lexicon`)
+
- **ๅฎšไน‰่งฃๆž**: ่งฃๆž Lexicon JSON ๅฎšไน‰ๆ–‡ไปถ
+
- **ๆจกๅž‹็”Ÿๆˆ**: ๅŠจๆ€็”Ÿๆˆ Pydantic ๆจกๅž‹็ฑป
+
- **ๅผ•็”จ่งฃๆž**: ๅค„็†่ทจๅฎšไน‰ๅผ•็”จๅ’Œ่”ๅˆ็ฑปๅž‹
+
- **ๆณจๅ†Œ็ฎก็†**: ๆจกๅž‹ๆณจๅ†Œ่กจๅ’Œ็ผ“ๅญ˜็ฎก็†
+
- **ๅ…ผๅฎนๆ€ง้ชŒ่ฏ**: ๅ‰ๅ‘ๅ’ŒๅŽๅ‘ๅ…ผๅฎนๆ€งๆฃ€ๆŸฅ
+
+
### 3. ๆ ธๅฟƒๅŠŸ่ƒฝ็‰นๆ€ง
+
+
#### 3.1 ็ฑปๅž‹ๅฎ‰ๅ…จ
+
- ๅŸบไบŽ Pydantic ็š„ๅผบ็ฑปๅž‹็ณป็ปŸ
+
- ่ฟ่กŒๆ—ถ็ฑปๅž‹้ชŒ่ฏ
+
- ่‡ชๅŠจ็ฑปๅž‹่ฝฌๆขๅ’Œ่ง„่ŒƒๅŒ–
+
+
#### 3.2 ๆ ผๅผๆ”ฏๆŒ
+
- **JSON**: ็ฌฆๅˆ ATProto JSON ็ผ–็ ่ง„่Œƒ
+
- **DAG-CBOR**: ๆ”ฏๆŒ่ง„่Œƒ็š„ DAG-CBOR ็ผ–็ 
+
- **ๆททๅˆๆ ผๅผ**: ๆ”ฏๆŒไธค็งๆ ผๅผ้—ด็š„่ฝฌๆข
+
+
#### 3.3 ้ชŒ่ฏ็ณป็ปŸ
+
- ่ฏญๆณ•้ชŒ่ฏ (ๅŸบ็ก€ๆ•ฐๆฎ็ฑปๅž‹)
+
- ่ฏญไน‰้ชŒ่ฏ (ไธšๅŠก่ง„ๅˆ™ๅ’Œ็บฆๆŸ)
+
- ๆ ผๅผ้ชŒ่ฏ (ๅญ—็ฌฆไธฒๆ ผๅผๅฆ‚ datetimeใ€uriใ€did ็ญ‰)
+
- ๅผ•็”จ้ชŒ่ฏ (CIDใ€blobใ€่ทจๅฎšไน‰ๅผ•็”จ)
+
+
### 4. ้›†ๆˆๆžถๆž„
+
+
#### 4.1 ไธŽ็Žฐๆœ‰ URI ๆจกๅ—็š„้›†ๆˆ
+
+
```python
+
# ็คบไพ‹๏ผšURI ไธŽ Data ๆจกๅ—็š„้›†ๆˆ
+
from atpasser.uri import URI, NSID
+
from atpasser.data import ATProtoSerializer
+
from atpasser.lexicon import LexiconRegistry
+
+
# ่งฃๆž URI
+
uri = URI("at://example.com/com.example.blog.post/123")
+
+
# ๆ นๆฎ NSID ่Žทๅ–ๅฏนๅบ”็š„ๆ•ฐๆฎๆจกๅž‹
+
model_class = LexiconRegistry.get_model(uri.collection.nsid)
+
+
# ไฝฟ็”จ Data ๆจกๅ—ๅค„็†ๆ•ฐๆฎ
+
serializer = ATProtoSerializer()
+
data = serializer.from_json(raw_data, model_class)
+
```
+
+
#### 4.2 ๆ•ฐๆฎๆตๆžถๆž„
+
+
```
+
ๅŽŸๅง‹ๆ•ฐๆฎ โ†’ Data ๆจกๅ—้ชŒ่ฏ โ†’ Lexicon ๆจกๅž‹่ฝฌๆข โ†’ ๅบๅˆ—ๅŒ–่พ“ๅ‡บ
+
Lexicon JSON โ†’ Lexicon ๆจกๅ—่งฃๆž โ†’ ็”Ÿๆˆ Pydantic ๆจกๅž‹ โ†’ ๆณจๅ†Œๅˆฐๆณจๅ†Œ่กจ
+
```
+
+
### 5. ้”™่ฏฏๅค„็†ๆžถๆž„
+
+
#### 5.1 ็ปŸไธ€็š„้”™่ฏฏไฝ“็ณป
+
+
```python
+
class ATProtoError(Exception):
+
"""ๅŸบ็ก€้”™่ฏฏ็ฑป"""
+
pass
+
+
class DataError(ATProtoError):
+
"""ๆ•ฐๆฎ็›ธๅ…ณ้”™่ฏฏ"""
+
pass
+
+
class LexiconError(ATProtoError):
+
"""Lexicon ็›ธๅ…ณ้”™่ฏฏ"""
+
pass
+
+
class URIError(ATProtoError):
+
"""URI ็›ธๅ…ณ้”™่ฏฏ"""
+
pass
+
```
+
+
#### 5.2 ้”™่ฏฏ่ฏŠๆ–ญ
+
- **ๅญ—ๆฎต็บง้”™่ฏฏๅฎšไฝ**: ็ฒพ็กฎๅˆฐๅ…ทไฝ“ๅญ—ๆฎต็š„่ทฏๅพ„ไฟกๆฏ
+
- **ไธŠไธ‹ๆ–‡ไฟกๆฏ**: ๅŒ…ๅซ้ชŒ่ฏๆ—ถ็š„่พ“ๅ…ฅๆ•ฐๆฎๅ’ŒๆœŸๆœ›ๆ ผๅผ
+
- **ๅปบ่ฎฎไฟฎๅค**: ๆไพ›ๅ…ทไฝ“็š„ไฟฎๅคๅปบ่ฎฎ
+
+
### 6. ๆ€ง่ƒฝไผ˜ๅŒ–็ญ–็•ฅ
+
+
#### 6.1 ็ผ“ๅญ˜ๆœบๅˆถ
+
- **ๆจกๅž‹็ผ“ๅญ˜**: ็ผ“ๅญ˜ๅทฒ่งฃๆž็š„ Lexicon ๆจกๅž‹
+
- **ๅบๅˆ—ๅŒ–็ผ“ๅญ˜**: ็ผ“ๅญ˜ๅบๅˆ—ๅŒ–็ป“ๆžœ
+
- **ๅผ•็”จ่งฃๆž็ผ“ๅญ˜**: ็ผ“ๅญ˜่ทจๅฎšไน‰ๅผ•็”จ่งฃๆž็ป“ๆžœ
+
+
#### 6.2 ๆ‡’ๅŠ ่ฝฝ
+
- ๆŒ‰้œ€่งฃๆž Lexicon ๅฎšไน‰
+
- ๅปถ่ฟŸๆจกๅž‹็”Ÿๆˆ็›ดๅˆฐๅฎž้™…ไฝฟ็”จ
+
- ๅŠจๆ€ๅฏผๅ…ฅไพ่ต–ๆจกๅ—
+
+
### 7. ๆ‰ฉๅฑ•ๆ€ง่ฎพ่ฎก
+
+
#### 7.1 ๆ’ไปถ็ณป็ปŸ
+
- ๆ”ฏๆŒ่‡ชๅฎšไน‰็ฑปๅž‹ๅค„็†ๅ™จ
+
- ๆ”ฏๆŒ่‡ชๅฎšไน‰้ชŒ่ฏ่ง„ๅˆ™
+
- ๆ”ฏๆŒ่‡ชๅฎšไน‰ๅบๅˆ—ๅŒ–ๆ ผๅผ
+
+
#### 7.2 ไธญ้—ดไปถๆ”ฏๆŒ
+
- ้ข„ๅค„็†้’ฉๅญ (ๆ•ฐๆฎๆธ…ๆด—ใ€่ฝฌๆข)
+
- ๅŽๅค„็†้’ฉๅญ (ๆ—ฅๅฟ—่ฎฐๅฝ•ใ€็›‘ๆŽง)
+
- ้ชŒ่ฏ้’ฉๅญ (่‡ชๅฎšไน‰้ชŒ่ฏ้€ป่พ‘)
+
+
### 8. ๅฎžๆ–ฝ่ทฏ็บฟๅ›พ
+
+
#### ้˜ถๆฎต 1: ๅŸบ็ก€ๅฎž็Žฐ (2-3 ๅ‘จ)
+
- ๅฎž็Žฐ Data ๆจกๅ—ๅŸบ็ก€็ฑปๅž‹ๅ’Œ JSON ๅบๅˆ—ๅŒ–
+
- ๅฎž็Žฐ Lexicon ๆจกๅ—ๅŸบ็ก€่งฃๆžๅ™จ
+
- ๅปบ็ซ‹ๅŸบๆœฌ็š„้”™่ฏฏๅค„็†็ณป็ปŸ
+
+
#### ้˜ถๆฎต 2: ๅฎŒๆ•ดๅŠŸ่ƒฝ (3-4 ๅ‘จ)
+
- ๆทปๅŠ  CBOR ๅบๅˆ—ๅŒ–ๆ”ฏๆŒ
+
- ๅฎž็ŽฐๅฎŒๆ•ด็š„้ชŒ่ฏ็ณป็ปŸ
+
- ๆทปๅŠ ๅผ•็”จ่งฃๆžๅ’Œ่”ๅˆ็ฑปๅž‹ๆ”ฏๆŒ
+
+
#### ้˜ถๆฎต 3: ไผ˜ๅŒ–ๅขžๅผบ (2 ๅ‘จ)
+
- ๅฎž็Žฐ็ผ“ๅญ˜ๅ’Œๆ€ง่ƒฝไผ˜ๅŒ–
+
- ๆทปๅŠ ้ซ˜็บงๆ ผๅผ้ชŒ่ฏ
+
- ๅฎŒๅ–„้”™่ฏฏๅค„็†ๅ’Œ่ฏŠๆ–ญไฟกๆฏ
+
+
#### ้˜ถๆฎต 4: ๆต‹่ฏ•้ƒจ็ฝฒ (1-2 ๅ‘จ)
+
- ็ผ–ๅ†™ๅฎŒๆ•ด็š„ๆต‹่ฏ•ๅฅ—ไปถ
+
- ๆ€ง่ƒฝๆต‹่ฏ•ๅ’Œไผ˜ๅŒ–
+
- ๆ–‡ๆกฃ็ผ–ๅ†™ๅ’Œ็คบไพ‹ไปฃ็ 
+
+
### 9. ไพ่ต–็ฎก็†
+
+
#### 9.1 ๆ ธๅฟƒไพ่ต–
+
- `pydantic >=2.11.9`: ๆ•ฐๆฎ้ชŒ่ฏๅ’Œๆจกๅž‹ๅฎšไน‰
+
- `cbor2 >=5.7.0`: CBOR ๅบๅˆ—ๅŒ–ๆ”ฏๆŒ
+
- `py-cid >=0.3.0`: CID ๅค„็†ๆ”ฏๆŒ
+
+
#### 9.2 ๅฏ้€‰ไพ่ต–
+
- `jsonpath-ng >=1.7.0`: JSONPath ๆ”ฏๆŒ
+
- `langcodes >=3.5.0`: ่ฏญ่จ€ไปฃ็ ้ชŒ่ฏ
+
+
### 10. ่ดจ้‡ไฟ่ฏ
+
+
#### 10.1 ๆต‹่ฏ•็ญ–็•ฅ
+
- **ๅ•ๅ…ƒๆต‹่ฏ•**: ่ฆ†็›–ๆ‰€ๆœ‰ๆ ธๅฟƒๅŠŸ่ƒฝ
+
- **้›†ๆˆๆต‹่ฏ•**: ๆต‹่ฏ•ๆจกๅ—้—ด้›†ๆˆ
+
- **ๅ…ผๅฎนๆ€งๆต‹่ฏ•**: ็กฎไฟไธŽ่ง„่Œƒๅ…ผๅฎน
+
- **ๆ€ง่ƒฝๆต‹่ฏ•**: ้ชŒ่ฏๆ€ง่ƒฝๆŒ‡ๆ ‡
+
+
#### 10.2 ไปฃ็ ่ดจ้‡
+
- ็ฑปๅž‹ๆณจ่งฃ่ฆ†็›–็އ่พพๅˆฐ 100%
+
- ๆต‹่ฏ•่ฆ†็›–็އ่ถ…่ฟ‡ 90%
+
- ้ตๅพช PEP 8 ็ผ–็ ่ง„่Œƒ
+
- ่ฏฆ็ป†็š„ๆ–‡ๆกฃๅ’Œ็คบไพ‹
+
+
## ๆ€ป็ป“
+
+
ๆœฌๆžถๆž„่ฎพ่ฎกๆไพ›ไบ†ไธ€ไธชๅฎŒๆ•ดใ€ๅฏๆ‰ฉๅฑ•็š„ ATProto ๆ•ฐๆฎๅค„็†่งฃๅ†ณๆ–นๆกˆ๏ผŒๅ……ๅˆ†ๅˆฉ็”จไบ† Python ็š„็ฑปๅž‹็ณป็ปŸๅ’Œ็Žฐๆœ‰็”Ÿๆ€๏ผŒๅŒๆ—ถไฟๆŒไบ†ไธŽ ATProto ่ง„่Œƒ็š„ๅฎŒๅ…จๅ…ผๅฎนๆ€งใ€‚ๆจกๅ—ๅŒ–็š„่ฎพ่ฎกไฝฟๅพ—ๅ„ไธช็ป„ไปถๅฏไปฅ็‹ฌ็ซ‹ๅผ€ๅ‘ๅ’Œๆต‹่ฏ•๏ผŒๅŒๆ—ถไนŸไพฟไบŽๆœชๆฅ็š„ๆ‰ฉๅฑ•ๅ’Œ็ปดๆŠคใ€‚
+119
examples/basic_usage.py
···
···
+
"""Basic usage examples for ATProto data and lexicon modules."""
+
+
import json
+
from atpasser.data import serializer, CIDLink, DateTimeString
+
from atpasser.lexicon import parser, registry
+
+
+
def demonstrate_data_serialization():
+
"""Demonstrate basic data serialization."""
+
print("=== Data Serialization Demo ===")
+
+
# Create some sample data
+
sample_data = {
+
"title": "Hello ATProto",
+
"content": "This is a test post",
+
"createdAt": "2024-01-15T10:30:00.000Z",
+
"tags": ["atproto", "test", "demo"],
+
"cidLink": CIDLink(
+
"bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a"
+
),
+
}
+
+
# Serialize to JSON
+
json_output = serializer.to_json(sample_data, indent=2)
+
print("JSON Output:")
+
print(json_output)
+
+
# Deserialize back
+
deserialized = serializer.from_json(json_output)
+
print("\nDeserialized:")
+
print(deserialized)
+
+
print()
+
+
+
def demonstrate_lexicon_parsing():
+
"""Demonstrate Lexicon parsing."""
+
print("=== Lexicon Parsing Demo ===")
+
+
# Sample Lexicon definition
+
sample_lexicon = {
+
"lexicon": 1,
+
"id": "com.example.blog.post",
+
"description": "A simple blog post record",
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "literal:post",
+
"record": {
+
"type": "object",
+
"properties": {
+
"title": {"type": "string", "maxLength": 300},
+
"content": {"type": "string"},
+
"createdAt": {"type": "string", "format": "datetime"},
+
"tags": {
+
"type": "array",
+
"items": {"type": "string"},
+
"maxLength": 10,
+
},
+
},
+
"required": ["title", "content", "createdAt"],
+
},
+
}
+
},
+
}
+
+
try:
+
# Parse and register the Lexicon
+
parser.parse_and_register(sample_lexicon)
+
print("Lexicon parsed and registered successfully!")
+
+
# Get the generated model
+
model_class = registry.get_model("com.example.blog.post")
+
if model_class:
+
print(f"Generated model: {model_class.__name__}")
+
+
# Create an instance using the model
+
post_data = {
+
"title": "Test Post",
+
"content": "This is a test post content",
+
"createdAt": "2024-01-15T10:30:00.000Z",
+
"tags": ["test", "demo"],
+
}
+
+
validated_post = model_class(**post_data)
+
print(f"Validated post: {validated_post.model_dump()}")
+
+
except Exception as e:
+
print(f"Error: {e}")
+
+
print()
+
+
+
def demonstrate_custom_types():
+
"""Demonstrate custom type validation."""
+
print("=== Custom Type Validation Demo ===")
+
+
# DateTimeString validation
+
try:
+
valid_dt = DateTimeString("2024-01-15T10:30:00.000Z")
+
print(f"Valid datetime: {valid_dt}")
+
except Exception as e:
+
print(f"DateTime validation error: {e}")
+
+
# Invalid datetime
+
try:
+
invalid_dt = DateTimeString("invalid-date")
+
print(f"Invalid datetime: {invalid_dt}")
+
except Exception as e:
+
print(f"DateTime validation caught: {e}")
+
+
print()
+
+
+
if __name__ == "__main__":
+
demonstrate_data_serialization()
+
demonstrate_lexicon_parsing()
+
demonstrate_custom_types()
+
print("Demo completed!")
+11
src/atpasser/__init__.py
···
···
+
"""ATProto Python implementation - Tools for Authenticated Transfer Protocol."""
+
+
from . import uri
+
from . import data
+
from . import lexicon
+
+
__all__ = ["uri", "data", "lexicon"]
+
+
__version__ = "0.1.0"
+
__author__ = "diaowinner"
+
__email__ = "diaowinner@qq.com"
+215
src/atpasser/data/ARCHITECTURE.md
···
···
+
# ATProto ๆ•ฐๆฎๆจกๅž‹ๆจกๅ—ๆžถๆž„่ฎพ่ฎก
+
+
## ๆฆ‚่ฟฐ
+
+
ๆœฌๆจกๅ—่ดŸ่ดฃๅฎž็Žฐ ATProto ๆ•ฐๆฎๆจกๅž‹็š„ๅบๅˆ—ๅŒ–ใ€ๅๅบๅˆ—ๅŒ–ๅ’Œ้ชŒ่ฏๅŠŸ่ƒฝ๏ผŒๆ”ฏๆŒ JSON ๅ’Œ DAG-CBOR ไธค็งๆ ผๅผ็š„ๆ•ฐๆฎ็ผ–็ ใ€‚
+
+
## ๆ ธๅฟƒๆžถๆž„่ฎพ่ฎก
+
+
### 1. ๅŸบ็ก€็ฑปๅž‹็ณป็ปŸ
+
+
#### 1.1 ๅŸบ็ก€็ฑปๅž‹ๆ˜ ๅฐ„
+
+
```python
+
# ๅŸบ็ก€็ฑปๅž‹ๆ˜ ๅฐ„
+
DATA_MODEL_TYPE_MAPPING = {
+
"null": NoneType,
+
"boolean": bool,
+
"integer": int,
+
"string": str,
+
"bytes": bytes,
+
"cid-link": CIDLink,
+
"blob": BlobRef,
+
"array": list,
+
"object": dict
+
}
+
```
+
+
#### 1.2 ่‡ชๅฎšไน‰ๅญ—ๆฎต็ฑปๅž‹
+
+
- **CIDLink**: ๅค„็† CID ้“พๆŽฅ๏ผŒๆ”ฏๆŒไบŒ่ฟ›ๅˆถๅ’Œๅญ—็ฌฆไธฒ่กจ็คบ
+
- **BlobRef**: ๅค„็† blob ๅผ•็”จ๏ผŒๆ”ฏๆŒๆ–ฐๆ—งๆ ผๅผๅ…ผๅฎน
+
- **DateTimeString**: RFC 3339 ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ้ชŒ่ฏ
+
- **LanguageTag**: BCP 47 ่ฏญ่จ€ๆ ‡็ญพ้ชŒ่ฏ
+
+
### 2. ๅบๅˆ—ๅŒ–ๅ™จๆžถๆž„
+
+
#### 2.1 ๅบๅˆ—ๅŒ–ๅ™จๅฑ‚็บง็ป“ๆž„
+
+
```
+
ATProtoSerializer
+
โ”œโ”€โ”€ JSONSerializer
+
โ”‚ โ”œโ”€โ”€ Normalizer
+
โ”‚ โ””โ”€โ”€ Denormalizer
+
โ””โ”€โ”€ CBORSerializer
+
โ”œโ”€โ”€ DAGCBOREncoder
+
โ””โ”€โ”€ DAGCBORDecoder
+
```
+
+
#### 2.2 ๅบๅˆ—ๅŒ–ๆต็จ‹
+
+
1. **ๆ•ฐๆฎ้ชŒ่ฏ**: ไฝฟ็”จ Pydantic ๆจกๅž‹้ชŒ่ฏๆ•ฐๆฎ
+
2. **ๆ ผๅผ่ฝฌๆข**: ็‰นๆฎŠ็ฑปๅž‹่ฝฌๆข๏ผˆCIDใ€bytes ็ญ‰๏ผ‰
+
3. **็ผ–็ **: ๆ นๆฎ็›ฎๆ ‡ๆ ผๅผ่ฟ›่กŒ็ผ–็ 
+
4. **่ง„่ŒƒๅŒ–**: ็กฎไฟ่พ“ๅ‡บ็ฌฆๅˆ ATProto ่ง„่Œƒ
+
+
### 3. ้ชŒ่ฏ็ณป็ปŸ
+
+
#### 3.1 ้ชŒ่ฏๅฑ‚็บง
+
+
1. **่ฏญๆณ•้ชŒ่ฏ**: ๅŸบ็ก€ๆ•ฐๆฎ็ฑปๅž‹้ชŒ่ฏ
+
2. **ๆ ผๅผ้ชŒ่ฏ**: ๅญ—็ฌฆไธฒๆ ผๅผ้ชŒ่ฏ๏ผˆdatetimeใ€uriใ€did ็ญ‰๏ผ‰
+
3. **็บฆๆŸ้ชŒ่ฏ**: ้•ฟๅบฆใ€่Œƒๅ›ดใ€ๆžšไธพ็ญ‰็บฆๆŸ้ชŒ่ฏ
+
4. **ๅผ•็”จ้ชŒ่ฏ**: CID ๅ’Œ blob ๅผ•็”จๆœ‰ๆ•ˆๆ€ง้ชŒ่ฏ
+
+
#### 3.2 ่‡ชๅฎšไน‰้ชŒ่ฏๅ™จ
+
+
```python
+
class DataModelValidator:
+
def validate_cid(self, value: str) -> bool:
+
"""้ชŒ่ฏ CID ๆ ผๅผ"""
+
pass
+
+
def validate_datetime(self, value: str) -> bool:
+
"""้ชŒ่ฏ RFC 3339 datetime ๆ ผๅผ"""
+
pass
+
+
def validate_did(self, value: str) -> bool:
+
"""้ชŒ่ฏ DID ๆ ผๅผ"""
+
pass
+
+
def validate_handle(self, value: str) -> bool:
+
"""้ชŒ่ฏ handle ๆ ผๅผ"""
+
pass
+
+
def validate_nsid(self, value: str) -> bool:
+
"""้ชŒ่ฏ NSID ๆ ผๅผ"""
+
pass
+
```
+
+
### 4. ็‰นๆฎŠ็ฑปๅž‹ๅค„็†
+
+
#### 4.1 CID ้“พๆŽฅๅค„็†
+
+
```python
+
class CIDLink:
+
"""ๅค„็† CID ้“พๆŽฅ็ฑปๅž‹"""
+
+
def __init__(self, cid: Union[str, bytes]):
+
self.cid = cid
+
+
def to_json(self) -> dict:
+
"""ๅบๅˆ—ๅŒ–ไธบ JSON ๆ ผๅผ: {"$link": "cid-string"}"""
+
return {"$link": str(self.cid)}
+
+
def to_cbor(self) -> bytes:
+
"""ๅบๅˆ—ๅŒ–ไธบ DAG-CBOR ๆ ผๅผ"""
+
pass
+
```
+
+
#### 4.2 Blob ๅผ•็”จๅค„็†
+
+
```python
+
class BlobRef:
+
"""ๅค„็† blob ๅผ•็”จ๏ผŒๆ”ฏๆŒๆ–ฐๆ—งๆ ผๅผ"""
+
+
def __init__(self, ref: CIDLink, mime_type: str, size: int):
+
self.ref = ref
+
self.mime_type = mime_type
+
self.size = size
+
+
def to_json(self) -> dict:
+
"""ๅบๅˆ—ๅŒ–ไธบ JSON ๆ ผๅผ"""
+
return {
+
"$type": "blob",
+
"ref": self.ref.to_json(),
+
"mimeType": self.mime_type,
+
"size": self.size
+
}
+
+
@classmethod
+
def from_legacy(cls, data: dict):
+
"""ไปŽๆ—งๆ ผๅผ่งฃๆž"""
+
pass
+
```
+
+
### 5. ้”™่ฏฏๅค„็†็ณป็ปŸ
+
+
#### 5.1 ้”™่ฏฏ็ฑปๅž‹ไฝ“็ณป
+
+
```python
+
class DataModelError(Exception):
+
"""ๅŸบ็ก€ๆ•ฐๆฎๆจกๅž‹้”™่ฏฏ"""
+
pass
+
+
class SerializationError(DataModelError):
+
"""ๅบๅˆ—ๅŒ–้”™่ฏฏ"""
+
pass
+
+
class ValidationError(DataModelError):
+
"""้ชŒ่ฏ้”™่ฏฏ"""
+
pass
+
+
class FormatError(DataModelError):
+
"""ๆ ผๅผ้”™่ฏฏ"""
+
pass
+
```
+
+
#### 5.2 ้”™่ฏฏๆถˆๆฏๆ ผๅผ
+
+
- **่ฏฆ็ป†่ทฏๅพ„ไฟกๆฏ**: ๅŒ…ๅซๅญ—ๆฎต่ทฏๅพ„
+
- **ๆœŸๆœ›ๅ€ผๆ่ฟฐ**: ๆ˜Ž็กฎ็š„ๆœŸๆœ›ๆ ผๅผ่ฏดๆ˜Ž
+
- **ไธŠไธ‹ๆ–‡ไฟกๆฏ**: ้ชŒ่ฏๆ—ถ็š„ไธŠไธ‹ๆ–‡ๆ•ฐๆฎ
+
+
### 6. ๆจกๅ—ๆ–‡ไปถ็ป“ๆž„
+
+
```
+
src/atpasser/data/
+
โ”œโ”€โ”€ __init__.py # ๆจกๅ—ๅฏผๅ‡บ
+
โ”œโ”€โ”€ ARCHITECTURE.md # ๆžถๆž„ๆ–‡ๆกฃ
+
โ”œโ”€โ”€ types.py # ๅŸบ็ก€็ฑปๅž‹ๅฎšไน‰
+
โ”œโ”€โ”€ serializer.py # ๅบๅˆ—ๅŒ–ๅ™จๅฎž็Žฐ
+
โ”œโ”€โ”€ validator.py # ้ชŒ่ฏๅ™จๅฎž็Žฐ
+
โ”œโ”€โ”€ exceptions.py # ๅผ‚ๅธธๅฎšไน‰
+
โ”œโ”€โ”€ cid.py # CID ้“พๆŽฅๅค„็†
+
โ”œโ”€โ”€ blob.py # Blob ๅผ•็”จๅค„็†
+
โ””โ”€โ”€ formats.py # ๆ ผๅผ้ชŒ่ฏๅ™จ
+
```
+
+
### 7. ไพ่ต–ๅ…ณ็ณป
+
+
- **ๅ†…้ƒจไพ่ต–**: `src/atpasser/uri` (NSIDใ€DIDใ€Handle ้ชŒ่ฏ)
+
- **ๅค–้ƒจไพ่ต–**:
+
- `pydantic`: ๆ•ฐๆฎ้ชŒ่ฏ
+
- `cbor2`: CBOR ๅบๅˆ—ๅŒ–
+
- `py-cid`: CID ๅค„็†
+
+
## ๅฎž็Žฐ็ญ–็•ฅ
+
+
### 1. ๆธ่ฟ›ๅผๅฎž็Žฐ
+
+
1. **้˜ถๆฎตไธ€**: ๅฎž็ŽฐๅŸบ็ก€็ฑปๅž‹ๅ’Œ JSON ๅบๅˆ—ๅŒ–
+
2. **้˜ถๆฎตไบŒ**: ๆทปๅŠ  CBOR ๅบๅˆ—ๅŒ–ๅ’Œ้ชŒ่ฏๅ™จ
+
3. **้˜ถๆฎตไธ‰**: ๅฎž็Žฐ้ซ˜็บงๆ ผๅผ้ชŒ่ฏ
+
4. **้˜ถๆฎตๅ››**: ๆ€ง่ƒฝไผ˜ๅŒ–ๅ’Œๅ†…ๅญ˜็ฎก็†
+
+
### 2. ๆต‹่ฏ•็ญ–็•ฅ
+
+
- **ๅ•ๅ…ƒๆต‹่ฏ•**: ๆต‹่ฏ•ๅ„ไธช็ป„ไปถๅŠŸ่ƒฝ
+
- **้›†ๆˆๆต‹่ฏ•**: ๆต‹่ฏ•็ซฏๅˆฐ็ซฏๆ•ฐๆฎๆต
+
- **ๅ…ผๅฎนๆ€งๆต‹่ฏ•**: ็กฎไฟไธŽ็Žฐๆœ‰ๅฎž็Žฐๅ…ผๅฎน
+
- **ๆ€ง่ƒฝๆต‹่ฏ•**: ้ชŒ่ฏๅบๅˆ—ๅŒ–ๆ€ง่ƒฝ
+
+
### 3. ๆ‰ฉๅฑ•ๆ€ง่€ƒ่™‘
+
+
- **ๆ’ไปถ็ณป็ปŸ**: ๆ”ฏๆŒ่‡ชๅฎšไน‰ๆ ผๅผ้ชŒ่ฏ
+
- **ไธญ้—ดไปถ**: ๆ”ฏๆŒ้ข„ๅค„็†ๅ’ŒๅŽๅค„็†้’ฉๅญ
+
- **็ผ“ๅญ˜**: ๅบๅˆ—ๅŒ–็ป“ๆžœ็ผ“ๅญ˜ไผ˜ๅŒ–
+
+
## ไผ˜ๅŠฟ
+
+
1. **็ฑปๅž‹ๅฎ‰ๅ…จ**: ๅŸบไบŽ Pydantic ็š„ๅผบ็ฑปๅž‹็ณป็ปŸ
+
2. **ๆ€ง่ƒฝ**: ไผ˜ๅŒ–็š„ๅบๅˆ—ๅŒ–ๅฎž็Žฐ
+
3. **ๅ…ผๅฎนๆ€ง**: ๆ”ฏๆŒๆ–ฐๆ—งๆ ผๅผๅ…ผๅฎน
+
4. **ๅฏๆ‰ฉๅฑ•**: ๆจกๅ—ๅŒ–่ฎพ่ฎกๆ”ฏๆŒๆœชๆฅๆ‰ฉๅฑ•
+
5. **้”™่ฏฏๅ‹ๅฅฝ**: ่ฏฆ็ป†็š„้”™่ฏฏๆถˆๆฏๅ’Œ่ฏŠๆ–ญไฟกๆฏ
+47
src/atpasser/data/__init__.py
···
···
+
"""ATProto data model module for serialization and validation."""
+
+
from .exceptions import (
+
DataModelError,
+
SerializationError,
+
ValidationError,
+
FormatError,
+
CIDError,
+
BlobError,
+
)
+
+
from .types import (
+
CIDLink,
+
DateTimeString,
+
LanguageTag,
+
ATUri,
+
DIDString,
+
HandleString,
+
NSIDString,
+
)
+
+
from .formats import format_validator, FormatValidator
+
from .serializer import ATProtoSerializer, serializer
+
+
__all__ = [
+
# Exceptions
+
"DataModelError",
+
"SerializationError",
+
"ValidationError",
+
"FormatError",
+
"CIDError",
+
"BlobError",
+
# Types
+
"CIDLink",
+
"DateTimeString",
+
"LanguageTag",
+
"ATUri",
+
"DIDString",
+
"HandleString",
+
"NSIDString",
+
# Validators
+
"format_validator",
+
"FormatValidator",
+
# Serializers
+
"ATProtoSerializer",
+
"serializer",
+
]
+87
src/atpasser/data/exceptions.py
···
···
+
"""Exceptions for ATProto data model module."""
+
+
from typing import Optional
+
+
+
class DataModelError(Exception):
+
"""Base exception for data model errors."""
+
+
def __init__(self, message: str, details: Optional[str] = None):
+
self.message = message
+
self.details = details
+
super().__init__(message)
+
+
+
class SerializationError(DataModelError):
+
"""Raised when serialization fails."""
+
+
def __init__(self, message: str, details: Optional[str] = None):
+
super().__init__(f"Serialization error: {message}", details)
+
+
+
class ValidationError(DataModelError):
+
"""Raised when data validation fails."""
+
+
def __init__(
+
self,
+
message: str,
+
field_path: Optional[str] = None,
+
expected: Optional[str] = None,
+
actual: Optional[str] = None,
+
):
+
self.fieldPath = field_path
+
self.expected = expected
+
self.actual = actual
+
+
details = []
+
if field_path:
+
details.append(f"Field: {field_path}")
+
if expected:
+
details.append(f"Expected: {expected}")
+
if actual:
+
details.append(f"Actual: {actual}")
+
+
super().__init__(
+
f"Validation error: {message}", "; ".join(details) if details else None
+
)
+
+
+
class FormatError(DataModelError):
+
"""Raised when format validation fails."""
+
+
def __init__(
+
self,
+
message: str,
+
format_type: Optional[str] = None,
+
value: Optional[str] = None,
+
):
+
self.formatType = format_type
+
self.value = value
+
+
details = []
+
if format_type:
+
details.append(f"Format: {format_type}")
+
if value:
+
details.append(f"Value: {value}")
+
+
super().__init__(
+
f"Format error: {message}", "; ".join(details) if details else None
+
)
+
+
+
class CIDError(DataModelError):
+
"""Raised when CID processing fails."""
+
+
def __init__(self, message: str, cid: Optional[str] = None):
+
self.cid = cid
+
super().__init__(f"CID error: {message}", f"CID: {cid}" if cid else None)
+
+
+
class BlobError(DataModelError):
+
"""Raised when blob processing fails."""
+
+
def __init__(self, message: str, blob_ref: Optional[str] = None):
+
self.blobRef = blob_ref
+
super().__init__(
+
f"Blob error: {message}", f"Blob ref: {blob_ref}" if blob_ref else None
+
)
+190
src/atpasser/data/formats.py
···
···
+
"""Format validators for ATProto data model."""
+
+
import re
+
from typing import Any, Optional
+
from .exceptions import FormatError
+
+
+
class FormatValidator:
+
"""Validates string formats according to ATProto specifications."""
+
+
@staticmethod
+
def validate_datetime(value: str) -> str:
+
"""Validate RFC 3339 datetime format."""
+
# RFC 3339 pattern with strict validation
+
pattern = (
+
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
+
)
+
if not re.match(pattern, value):
+
raise FormatError("Invalid RFC 3339 datetime format", "datetime", value)
+
+
# Additional semantic validation
+
try:
+
# Extract date parts for validation
+
date_part, time_part = value.split("T", 1)
+
year, month, day = map(int, date_part.split("-"))
+
+
# Basic date validation
+
if not (1 <= month <= 12):
+
raise FormatError("Month must be between 01 and 12", "datetime", value)
+
if not (1 <= day <= 31):
+
raise FormatError("Day must be between 01 and 31", "datetime", value)
+
if year < 0:
+
raise FormatError("Year must be positive", "datetime", value)
+
+
except ValueError:
+
raise FormatError("Invalid datetime structure", "datetime", value)
+
+
return value
+
+
@staticmethod
+
def validate_did(value: str) -> str:
+
"""Validate DID format."""
+
pattern = r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$"
+
if not re.match(pattern, value):
+
raise FormatError("Invalid DID format", "did", value)
+
+
if len(value) > 2048:
+
raise FormatError("DID too long", "did", value)
+
+
return value
+
+
@staticmethod
+
def validate_handle(value: str) -> str:
+
"""Validate handle format."""
+
if len(value) > 253:
+
raise FormatError("Handle too long", "handle", value)
+
+
labels = value.lower().split(".")
+
if len(labels) < 2:
+
raise FormatError("Handle must contain at least one dot", "handle", value)
+
+
for i, label in enumerate(labels):
+
if not (1 <= len(label) <= 63):
+
raise FormatError(
+
f"Label {i+1} length must be 1-63 characters", "handle", value
+
)
+
+
if not re.match(r"^[a-z0-9-]+$", label):
+
raise FormatError(
+
f"Label {i+1} contains invalid characters", "handle", value
+
)
+
+
if label.startswith("-") or label.endswith("-"):
+
raise FormatError(
+
f"Label {i+1} cannot start or end with hyphen", "handle", value
+
)
+
+
if labels[-1][0].isdigit():
+
raise FormatError("TLD cannot start with digit", "handle", value)
+
+
return value
+
+
@staticmethod
+
def validate_nsid(value: str) -> str:
+
"""Validate NSID format."""
+
if len(value) > 317:
+
raise FormatError("NSID too long", "nsid", value)
+
+
if not all(ord(c) < 128 for c in value):
+
raise FormatError("NSID must contain only ASCII characters", "nsid", value)
+
+
if value.startswith(".") or value.endswith("."):
+
raise FormatError("NSID cannot start or end with dot", "nsid", value)
+
+
segments = value.split(".")
+
if len(segments) < 3:
+
raise FormatError("NSID must have at least 3 segments", "nsid", value)
+
+
# Validate domain authority segments
+
for i, segment in enumerate(segments[:-1]):
+
if not (1 <= len(segment) <= 63):
+
raise FormatError(
+
f"Domain segment {i+1} length must be 1-63", "nsid", value
+
)
+
+
if not re.match(r"^[a-z0-9-]+$", segment):
+
raise FormatError(
+
f"Domain segment {i+1} contains invalid chars", "nsid", value
+
)
+
+
if segment.startswith("-") or segment.endswith("-"):
+
raise FormatError(
+
f"Domain segment {i+1} cannot start/end with hyphen", "nsid", value
+
)
+
+
# Validate name segment
+
name = segments[-1]
+
if not (1 <= len(name) <= 63):
+
raise FormatError("Name segment length must be 1-63", "nsid", value)
+
+
if not re.match(r"^[a-zA-Z0-9]+$", name):
+
raise FormatError("Name segment contains invalid characters", "nsid", value)
+
+
if name[0].isdigit():
+
raise FormatError("Name segment cannot start with digit", "nsid", value)
+
+
return value
+
+
@staticmethod
+
def validate_uri(value: str) -> str:
+
"""Validate URI format."""
+
if len(value) > 8192: # 8 KB limit
+
raise FormatError("URI too long", "uri", value)
+
+
# Basic URI pattern validation
+
uri_pattern = r"^[a-zA-Z][a-zA-Z0-9+.-]*:.*$"
+
if not re.match(uri_pattern, value):
+
raise FormatError("Invalid URI format", "uri", value)
+
+
return value
+
+
@staticmethod
+
def validate_cid(value: str) -> str:
+
"""Validate CID format."""
+
# Basic CID pattern validation (simplified)
+
cid_pattern = r"^[a-zA-Z0-9]+$"
+
if not re.match(cid_pattern, value):
+
raise FormatError("Invalid CID format", "cid", value)
+
+
return value
+
+
@staticmethod
+
def validate_at_identifier(value: str) -> str:
+
"""Validate at-identifier format (DID or handle)."""
+
try:
+
# Try DID first
+
return FormatValidator.validate_did(value)
+
except FormatError:
+
try:
+
# Fall back to handle
+
return FormatValidator.validate_handle(value)
+
except FormatError:
+
raise FormatError(
+
"Invalid at-identifier (not a DID or handle)",
+
"at-identifier",
+
value,
+
)
+
+
@staticmethod
+
def validate_at_uri(value: str) -> str:
+
"""Validate at-uri format."""
+
if not value.startswith("at://"):
+
raise FormatError("AT URI must start with 'at://'", "at-uri", value)
+
+
# Additional validation can be added here
+
return value
+
+
@staticmethod
+
def validate_language(value: str) -> str:
+
"""Validate language tag format."""
+
# BCP 47 pattern validation
+
pattern = r"^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$"
+
if not re.match(pattern, value):
+
raise FormatError("Invalid language tag format", "language", value)
+
+
return value
+
+
+
# Global validator instance
+
format_validator = FormatValidator()
+125
src/atpasser/data/serializer.py
···
···
+
"""Serializer for ATProto data model formats."""
+
+
import json
+
import base64
+
from typing import Any, Dict, Type, Union, Optional
+
from pydantic import BaseModel
+
from .exceptions import SerializationError, ValidationError
+
from .types import CIDLink
+
+
+
class ATProtoSerializer:
+
"""Serializer for ATProto JSON and CBOR formats."""
+
+
def __init__(self):
+
self.json_encoder = JSONEncoder()
+
self.json_decoder = JSONDecoder()
+
+
def to_json(self, obj: Any, indent: Optional[int] = None) -> str:
+
"""Serialize object to ATProto JSON format."""
+
try:
+
if isinstance(obj, BaseModel):
+
obj = obj.model_dump(mode="json")
+
+
serialized = self.json_encoder.encode(obj)
+
return json.dumps(serialized, indent=indent, ensure_ascii=False)
+
except Exception as e:
+
raise SerializationError(f"JSON serialization failed: {str(e)}")
+
+
def from_json(
+
self, data: Union[str, bytes, dict], model: Optional[Type[BaseModel]] = None
+
) -> Any:
+
"""Deserialize from ATProto JSON format."""
+
try:
+
if isinstance(data, (str, bytes)):
+
data = json.loads(data)
+
+
decoded = self.json_decoder.decode(data)
+
+
if model and issubclass(model, BaseModel):
+
return model.model_validate(decoded)
+
return decoded
+
except Exception as e:
+
raise SerializationError(f"JSON deserialization failed: {str(e)}")
+
+
def to_cbor(self, obj: Any) -> bytes:
+
"""Serialize object to DAG-CBOR format."""
+
try:
+
# This is a placeholder - actual CBOR implementation would go here
+
# For now, we'll convert to JSON and then encode as bytes
+
json_str = self.to_json(obj)
+
return json_str.encode("utf-8")
+
except Exception as e:
+
raise SerializationError(f"CBOR serialization failed: {str(e)}")
+
+
def from_cbor(self, data: bytes, model: Optional[Type[BaseModel]] = None) -> Any:
+
"""Deserialize from DAG-CBOR format."""
+
try:
+
# This is a placeholder - actual CBOR implementation would go here
+
# For now, we'll decode from bytes and then parse JSON
+
json_str = data.decode("utf-8")
+
return self.from_json(json_str, model)
+
except Exception as e:
+
raise SerializationError(f"CBOR deserialization failed: {str(e)}")
+
+
+
class JSONEncoder:
+
"""Encodes Python objects to ATProto JSON format."""
+
+
def encode(self, obj: Any) -> Any:
+
"""Recursively encode object to ATProto JSON format."""
+
if isinstance(obj, dict):
+
return {k: self.encode(v) for k, v in obj.items()}
+
elif isinstance(obj, list):
+
return [self.encode(item) for item in obj]
+
elif isinstance(obj, CIDLink):
+
return obj.to_json()
+
elif isinstance(obj, bytes):
+
return self._encode_bytes(obj)
+
else:
+
return obj
+
+
def _encode_bytes(self, data: bytes) -> Dict[str, str]:
+
"""Encode bytes to ATProto bytes format."""
+
return {"$bytes": base64.b64encode(data).decode("ascii")}
+
+
+
class JSONDecoder:
+
"""Decodes ATProto JSON format to Python objects."""
+
+
def decode(self, obj: Any) -> Any:
+
"""Recursively decode ATProto JSON format to Python objects."""
+
if isinstance(obj, dict):
+
return self._decode_object(obj)
+
elif isinstance(obj, list):
+
return [self.decode(item) for item in obj]
+
else:
+
return obj
+
+
def _decode_object(self, obj: Dict[str, Any]) -> Any:
+
"""Decode a JSON object, handling special ATProto formats."""
+
if len(obj) == 1:
+
key = next(iter(obj.keys()))
+
value = obj[key]
+
+
if key == "$link" and isinstance(value, str):
+
return CIDLink(value)
+
elif key == "$bytes" and isinstance(value, str):
+
return self._decode_bytes(value)
+
elif key == "$type" and value == "blob":
+
# This would be handled by a blob-specific decoder
+
return obj
+
+
# Regular object - decode recursively
+
return {k: self.decode(v) for k, v in obj.items()}
+
+
def _decode_bytes(self, value: str) -> bytes:
+
"""Decode ATProto bytes format."""
+
try:
+
return base64.b64decode(value)
+
except Exception as e:
+
raise SerializationError(f"Invalid base64 encoding: {str(e)}")
+
+
+
# Global serializer instance
+
serializer = ATProtoSerializer()
+263
src/atpasser/lexicon/ARCHITECTURE.md
···
···
+
# ATProto Lexicon ๆจกๅ—ๆžถๆž„่ฎพ่ฎก
+
+
## ๆฆ‚่ฟฐ
+
+
ๆœฌๆจกๅ—่ดŸ่ดฃ่งฃๆžใ€้ชŒ่ฏๅ’Œ็ฎก็† ATProto Lexicon ๅฎšไน‰ๆ–‡ไปถ๏ผŒๅฐ† JSON Schema ่ฝฌๆขไธบๅฏๆ‰ง่กŒ็š„ Pydantic ๆจกๅž‹๏ผŒๅนถๆไพ›็ฑปๅž‹ๅฎ‰ๅ…จ็š„ๆŽฅๅฃใ€‚
+
+
## ๆ ธๅฟƒๆžถๆž„่ฎพ่ฎก
+
+
### 1. Lexicon ่งฃๆž็ณป็ปŸ
+
+
#### 1.1 ่งฃๆžๅ™จๅฑ‚็บง็ป“ๆž„
+
+
```
+
LexiconParser
+
โ”œโ”€โ”€ DefinitionParser
+
โ”‚ โ”œโ”€โ”€ PrimaryDefinitionParser
+
โ”‚ โ”‚ โ”œโ”€โ”€ RecordParser
+
โ”‚ โ”‚ โ”œโ”€โ”€ QueryParser
+
โ”‚ โ”‚ โ”œโ”€โ”€ ProcedureParser
+
โ”‚ โ”‚ โ””โ”€โ”€ SubscriptionParser
+
โ”‚ โ””โ”€โ”€ FieldDefinitionParser
+
โ”‚ โ”œโ”€โ”€ SimpleTypeParser
+
โ”‚ โ”œโ”€โ”€ CompoundTypeParser
+
โ”‚ โ””โ”€โ”€ MetaTypeParser
+
โ””โ”€โ”€ Validator
+
โ”œโ”€โ”€ SchemaValidator
+
โ””โ”€โ”€ CrossReferenceValidator
+
```
+
+
#### 1.2 ่งฃๆžๆต็จ‹
+
+
1. **ๅŠ ่ฝฝ Lexicon JSON**: ่ฏปๅ–ๅนถ้ชŒ่ฏ Lexicon ๆ–‡ไปถ็ป“ๆž„
+
2. **่งฃๆžๅฎšไน‰**: ๆ นๆฎ็ฑปๅž‹ๅˆ†ๅ‘ๅˆฐ็›ธๅบ”็š„่งฃๆžๅ™จ
+
3. **ๆž„ๅปบๆจกๅž‹**: ็”Ÿๆˆๅฏนๅบ”็š„ Pydantic ๆจกๅž‹็ฑป
+
4. **้ชŒ่ฏๅผ•็”จ**: ๆฃ€ๆŸฅ่ทจๅฎšไน‰ๅผ•็”จ็š„ๆœ‰ๆ•ˆๆ€ง
+
5. **ๆณจๅ†Œๆจกๅž‹**: ๅฐ†ๆจกๅž‹ๆณจๅ†Œๅˆฐๅ…จๅฑ€ๆณจๅ†Œ่กจ
+
+
### 2. ็ฑปๅž‹ๆ˜ ๅฐ„็ณป็ปŸ
+
+
#### 2.1 Lexicon ็ฑปๅž‹ๅˆฐ Python ็ฑปๅž‹ๆ˜ ๅฐ„
+
+
```python
+
LEXICON_TYPE_MAPPING = {
+
"null": None,
+
"boolean": bool,
+
"integer": int,
+
"string": str,
+
"bytes": bytes,
+
"cid-link": "CIDLink",
+
"blob": "BlobRef",
+
"array": list,
+
"object": dict,
+
"params": dict,
+
"token": "LexiconToken",
+
"ref": "LexiconRef",
+
"union": "LexiconUnion",
+
"unknown": Any,
+
"record": "RecordModel",
+
"query": "QueryModel",
+
"procedure": "ProcedureModel",
+
"subscription": "SubscriptionModel"
+
}
+
```
+
+
#### 2.2 ่‡ชๅฎšไน‰็ฑปๅž‹ๅค„็†ๅ™จ
+
+
- **LexiconRef**: ๅค„็†่ทจๅฎšไน‰ๅผ•็”จ่งฃๆž
+
- **LexiconUnion**: ๅค„็†่”ๅˆ็ฑปๅž‹้ชŒ่ฏ
+
- **LexiconToken**: ๅค„็†็ฌฆๅทๅŒ–ๅ€ผ
+
- **RecordModel**: ่ฎฐๅฝ•็ฑปๅž‹ๅŸบ็ฑป
+
- **QueryModel**: ๆŸฅ่ฏข็ฑปๅž‹ๅŸบ็ฑป
+
+
### 3. ๆจกๅž‹็”Ÿๆˆ็ณป็ปŸ
+
+
#### 3.1 ๅŠจๆ€ๆจกๅž‹็”Ÿๆˆ
+
+
```python
+
class ModelGenerator:
+
"""ๅŠจๆ€็”Ÿๆˆ Pydantic ๆจกๅž‹"""
+
+
def generate_record_model(self, definition: dict) -> Type[BaseModel]:
+
"""็”Ÿๆˆ่ฎฐๅฝ•ๆจกๅž‹"""
+
pass
+
+
def generate_query_model(self, definition: dict) -> Type[BaseModel]:
+
"""็”ŸๆˆๆŸฅ่ฏขๆจกๅž‹"""
+
pass
+
+
def generate_field_validator(self, field_def: dict) -> Callable:
+
"""็”Ÿๆˆๅญ—ๆฎต้ชŒ่ฏๅ™จ"""
+
pass
+
```
+
+
#### 3.2 ็บฆๆŸๅค„็†
+
+
```python
+
class ConstraintProcessor:
+
"""ๅค„็†ๅญ—ๆฎต็บฆๆŸ"""
+
+
def process_integer_constraints(self, field_def: dict) -> dict:
+
"""ๅค„็†ๆ•ดๆ•ฐ็บฆๆŸ (min, max, enum)"""
+
pass
+
+
def process_string_constraints(self, field_def: dict) -> dict:
+
"""ๅค„็†ๅญ—็ฌฆไธฒ็บฆๆŸ (format, length, enum)"""
+
pass
+
+
def process_array_constraints(self, field_def: dict) -> dict:
+
"""ๅค„็†ๆ•ฐ็ป„็บฆๆŸ (minLength, maxLength)"""
+
pass
+
```
+
+
### 4. ๆณจๅ†Œ่กจๅ’Œ็ผ“ๅญ˜ๆœบๅˆถ
+
+
#### 4.1 ๆจกๅž‹ๆณจๅ†Œ่กจ
+
+
```python
+
class LexiconRegistry:
+
"""Lexicon ๆจกๅž‹ๆณจๅ†Œ่กจ"""
+
+
def __init__(self):
+
self._models: Dict[str, Type[BaseModel]] = {}
+
self._definitions: Dict[str, dict] = {}
+
self._ref_cache: Dict[str, Type[BaseModel]] = {}
+
+
def register(self, nsid: str, model: Type[BaseModel], definition: dict):
+
"""ๆณจๅ†Œ Lexicon ๆจกๅž‹"""
+
pass
+
+
def get_model(self, nsid: str) -> Optional[Type[BaseModel]]:
+
"""่Žทๅ–ๅทฒๆณจๅ†Œ็š„ๆจกๅž‹"""
+
pass
+
+
def resolve_ref(self, ref: str) -> Optional[Type[BaseModel]]:
+
"""่งฃๆžๅผ•็”จๅˆฐๅ…ทไฝ“ๆจกๅž‹"""
+
pass
+
+
def clear_cache(self):
+
"""ๆธ…็ฉบ็ผ“ๅญ˜"""
+
pass
+
```
+
+
#### 4.2 ็ผ“ๅญ˜็ญ–็•ฅ
+
+
- **ๅ†…ๅญ˜็ผ“ๅญ˜**: ็ผ“ๅญ˜ๅทฒ่งฃๆž็š„ๆจกๅž‹ๅฎšไน‰
+
- **ๆ–‡ไปถ็ผ“ๅญ˜**: ็ผ“ๅญ˜ๅบๅˆ—ๅŒ–็ป“ๆžœไปฅๆ้ซ˜ๆ€ง่ƒฝ
+
- **LRU ็ญ–็•ฅ**: ไฝฟ็”จๆœ€่ฟ‘ๆœ€ๅฐ‘ไฝฟ็”จ็ฎ—ๆณ•็ฎก็†็ผ“ๅญ˜
+
+
### 5. ้ชŒ่ฏ็ณป็ปŸ
+
+
#### 5.1 ้ชŒ่ฏๅฑ‚็บง
+
+
1. **่ฏญๆณ•้ชŒ่ฏ**: JSON Schema ็ป“ๆž„้ชŒ่ฏ
+
2. **่ฏญไน‰้ชŒ่ฏ**: ็ฑปๅž‹็บฆๆŸๅ’ŒไธšๅŠก่ง„ๅˆ™้ชŒ่ฏ
+
3. **ๅผ•็”จ้ชŒ่ฏ**: ่ทจๅฎšไน‰ๅผ•็”จๆœ‰ๆ•ˆๆ€ง้ชŒ่ฏ
+
4. **ๅ…ผๅฎนๆ€ง้ชŒ่ฏ**: ๅ‰ๅ‘ๅ’ŒๅŽๅ‘ๅ…ผๅฎนๆ€งๆฃ€ๆŸฅ
+
+
#### 5.2 ่‡ชๅฎšไน‰้ชŒ่ฏๅ™จ
+
+
```python
+
class LexiconValidator:
+
"""Lexicon ๅฎšไน‰้ชŒ่ฏๅ™จ"""
+
+
def validate_definition(self, definition: dict) -> bool:
+
"""้ชŒ่ฏ Lexicon ๅฎšไน‰ๅฎŒๆ•ดๆ€ง"""
+
pass
+
+
def validate_refs(self, definition: dict) -> List[str]:
+
"""้ชŒ่ฏๆ‰€ๆœ‰ๅผ•็”จ็š„ๆœ‰ๆ•ˆๆ€ง"""
+
pass
+
+
def validate_compatibility(self, old_def: dict, new_def: dict) -> bool:
+
"""้ชŒ่ฏ็‰ˆๆœฌๅ…ผๅฎนๆ€ง"""
+
pass
+
```
+
+
### 6. ้”™่ฏฏๅค„็†็ณป็ปŸ
+
+
#### 6.1 ้”™่ฏฏ็ฑปๅž‹ไฝ“็ณป
+
+
```python
+
class LexiconError(Exception):
+
"""ๅŸบ็ก€ Lexicon ้”™่ฏฏ"""
+
pass
+
+
class ParseError(LexiconError):
+
"""่งฃๆž้”™่ฏฏ"""
+
pass
+
+
class ValidationError(LexiconError):
+
"""้ชŒ่ฏ้”™่ฏฏ"""
+
pass
+
+
class ResolutionError(LexiconError):
+
"""ๅผ•็”จ่งฃๆž้”™่ฏฏ"""
+
pass
+
+
class GenerationError(LexiconError):
+
"""ๆจกๅž‹็”Ÿๆˆ้”™่ฏฏ"""
+
pass
+
```
+
+
#### 6.2 ่ฏŠๆ–ญไฟกๆฏ
+
+
- **่ฏฆ็ป†้”™่ฏฏๆถˆๆฏ**: ๅŒ…ๅซๅ…ทไฝ“็š„ๅญ—ๆฎต่ทฏๅพ„ๅ’ŒๆœŸๆœ›ๅ€ผ
+
- **ไธŠไธ‹ๆ–‡ไฟกๆฏ**: ๆไพ›้ชŒ่ฏๆ—ถ็š„ไธŠไธ‹ๆ–‡ไฟกๆฏ
+
- **ๅปบ่ฎฎไฟฎๅค**: ๆไพ›ๅฏ่ƒฝ็š„ไฟฎๅคๅปบ่ฎฎ
+
+
### 7. ๆจกๅ—ๆ–‡ไปถ็ป“ๆž„
+
+
```
+
src/atpasser/lexicon/
+
โ”œโ”€โ”€ __init__.py # ๆจกๅ—ๅฏผๅ‡บ
+
โ”œโ”€โ”€ ARCHITECTURE.md # ๆžถๆž„ๆ–‡ๆกฃ
+
โ”œโ”€โ”€ parser.py # ไธป่งฃๆžๅ™จ
+
โ”œโ”€โ”€ generator.py # ๆจกๅž‹็”Ÿๆˆๅ™จ
+
โ”œโ”€โ”€ registry.py # ๆณจๅ†Œ่กจๅฎž็Žฐ
+
โ”œโ”€โ”€ validator.py # ้ชŒ่ฏๅ™จๅฎž็Žฐ
+
โ”œโ”€โ”€ types.py # ็ฑปๅž‹ๅฎšไน‰
+
โ”œโ”€โ”€ exceptions.py # ๅผ‚ๅธธๅฎšไน‰
+
โ”œโ”€โ”€ constraints.py # ็บฆๆŸๅค„็†ๅ™จ
+
โ””โ”€โ”€ utils.py # ๅทฅๅ…ทๅ‡ฝๆ•ฐ
+
```
+
+
### 8. ไพ่ต–ๅ…ณ็ณป
+
+
- **ๅ†…้ƒจไพ่ต–**:
+
- `src/atpasser/data` (ๆ•ฐๆฎๅบๅˆ—ๅŒ–ๅ’Œ้ชŒ่ฏ)
+
- `src/atpasser/uri` (NSID ้ชŒ่ฏๅ’Œๅค„็†)
+
- **ๅค–้ƒจไพ่ต–**:
+
- `pydantic`: ๆจกๅž‹็”Ÿๆˆๅ’Œ้ชŒ่ฏ
+
- `jsonpath-ng`: JSONPath ๆ”ฏๆŒ
+
- `cbor2`: CBOR ๅบๅˆ—ๅŒ–ๆ”ฏๆŒ
+
+
## ๅฎž็Žฐ็ญ–็•ฅ
+
+
### 1. ๆธ่ฟ›ๅผๅฎž็Žฐ
+
+
1. **้˜ถๆฎตไธ€**: ๅฎž็ŽฐๅŸบ็ก€่งฃๆžๅ™จๅ’Œ็ฎ€ๅ•็ฑปๅž‹ๆ˜ ๅฐ„
+
2. **้˜ถๆฎตไบŒ**: ๆทปๅŠ ๅคๆ‚็ฑปๅž‹ๅ’Œๅผ•็”จ่งฃๆž
+
3. **้˜ถๆฎตไธ‰**: ๅฎž็Žฐๆจกๅž‹็”Ÿๆˆๅ’Œๆณจๅ†Œ่กจ
+
4. **้˜ถๆฎตๅ››**: ๆทปๅŠ ้ซ˜็บง้ชŒ่ฏๅ’Œ้”™่ฏฏๅค„็†
+
+
### 2. ๆต‹่ฏ•็ญ–็•ฅ
+
+
- **ๅ•ๅ…ƒๆต‹่ฏ•**: ๆต‹่ฏ•ๅ„ไธช่งฃๆžๅ™จ็ป„ไปถ
+
- **้›†ๆˆๆต‹่ฏ•**: ๆต‹่ฏ•็ซฏๅˆฐ็ซฏ Lexicon ่งฃๆžๆต็จ‹
+
- **ๅ…ผๅฎนๆ€งๆต‹่ฏ•**: ็กฎไฟไธŽ็Žฐๆœ‰ Lexicon ๆ–‡ไปถๅ…ผๅฎน
+
- **ๆ€ง่ƒฝๆต‹่ฏ•**: ้ชŒ่ฏ่งฃๆžๅ’Œๆจกๅž‹็”Ÿๆˆๆ€ง่ƒฝ
+
+
### 3. ๆ‰ฉๅฑ•ๆ€ง่€ƒ่™‘
+
+
- **ๆ’ไปถ็ณป็ปŸ**: ๆ”ฏๆŒ่‡ชๅฎšไน‰็ฑปๅž‹่งฃๆžๅ™จ
+
- **ไธญ้—ดไปถ**: ๆ”ฏๆŒ้ข„ๅค„็†ๅ’ŒๅŽๅค„็†้’ฉๅญ
+
- **็›‘ๆŽง**: ้›†ๆˆๆ€ง่ƒฝ็›‘ๆŽงๅ’Œๆ—ฅๅฟ—่ฎฐๅฝ•
+
+
## ไผ˜ๅŠฟ
+
+
1. **็ฑปๅž‹ๅฎ‰ๅ…จ**: ๅˆฉ็”จ Pydantic ็š„ๅผบ็ฑปๅž‹็ณป็ปŸ
+
2. **ๆ€ง่ƒฝ**: ไผ˜ๅŒ–็š„่งฃๆžๅ’Œ็ผ“ๅญ˜ๆœบๅˆถ
+
3. **ๅฏๆ‰ฉๅฑ•**: ๆจกๅ—ๅŒ–่ฎพ่ฎกๆ”ฏๆŒๆœชๆฅๆ‰ฉๅฑ•
+
4. **ๅ…ผๅฎนๆ€ง**: ไฟๆŒไธŽ ATProto Lexicon ่ง„่ŒƒๅฎŒๅ…จๅ…ผๅฎน
+
5. **ๅผ€ๅ‘่€…ๅ‹ๅฅฝ**: ๆไพ›ๆธ…ๆ™ฐ็š„้”™่ฏฏๆถˆๆฏๅ’Œๆ–‡ๆกฃ
+71
src/atpasser/lexicon/__init__.py
···
···
+
"""ATProto Lexicon module for parsing and managing schema definitions."""
+
+
from .exceptions import (
+
LexiconError,
+
ParseError,
+
ValidationError,
+
ResolutionError,
+
GenerationError,
+
CompatibilityError,
+
)
+
+
from .types import (
+
LexiconType,
+
LexiconDefinition,
+
IntegerConstraints,
+
StringConstraints,
+
ArrayConstraints,
+
ObjectConstraints,
+
BlobConstraints,
+
ParamsConstraints,
+
RefDefinition,
+
UnionDefinition,
+
RecordDefinition,
+
QueryDefinition,
+
ProcedureDefinition,
+
SubscriptionDefinition,
+
LexiconDocument,
+
ErrorDefinition,
+
LexiconSchema,
+
PropertyMap,
+
DefinitionMap,
+
)
+
+
from .registry import LexiconRegistry, registry
+
from .parser import LexiconParser, parser
+
+
__all__ = [
+
# Exceptions
+
"LexiconError",
+
"ParseError",
+
"ValidationError",
+
"ResolutionError",
+
"GenerationError",
+
"CompatibilityError",
+
# Types
+
"LexiconType",
+
"LexiconDefinition",
+
"IntegerConstraints",
+
"StringConstraints",
+
"ArrayConstraints",
+
"ObjectConstraints",
+
"BlobConstraints",
+
"ParamsConstraints",
+
"RefDefinition",
+
"UnionDefinition",
+
"RecordDefinition",
+
"QueryDefinition",
+
"ProcedureDefinition",
+
"SubscriptionDefinition",
+
"LexiconDocument",
+
"ErrorDefinition",
+
"LexiconSchema",
+
"PropertyMap",
+
"DefinitionMap",
+
# Registry
+
"LexiconRegistry",
+
"registry",
+
# Parser
+
"LexiconParser",
+
"parser",
+
]
+125
src/atpasser/lexicon/exceptions.py
···
···
+
"""Exceptions for ATProto Lexicon module."""
+
+
from typing import Optional
+
+
+
class LexiconError(Exception):
+
"""Base exception for Lexicon errors."""
+
+
def __init__(self, message: str, details: Optional[str] = None):
+
self.message = message
+
self.details = details
+
super().__init__(message)
+
+
+
class ParseError(LexiconError):
+
"""Raised when Lexicon parsing fails."""
+
+
def __init__(
+
self, message: str, nsid: Optional[str] = None, definition: Optional[str] = None
+
):
+
self.nsid = nsid
+
self.definition = definition
+
+
details = []
+
if nsid:
+
details.append(f"NSID: {nsid}")
+
if definition:
+
details.append(f"Definition: {definition}")
+
+
super().__init__(
+
f"Parse error: {message}", "; ".join(details) if details else None
+
)
+
+
+
class ValidationError(LexiconError):
+
"""Raised when Lexicon validation fails."""
+
+
def __init__(
+
self,
+
message: str,
+
nsid: Optional[str] = None,
+
field: Optional[str] = None,
+
expected: Optional[str] = None,
+
):
+
self.nsid = nsid
+
self.field = field
+
self.expected = expected
+
+
details = []
+
if nsid:
+
details.append(f"NSID: {nsid}")
+
if field:
+
details.append(f"Field: {field}")
+
if expected:
+
details.append(f"Expected: {expected}")
+
+
super().__init__(
+
f"Validation error: {message}", "; ".join(details) if details else None
+
)
+
+
+
class ResolutionError(LexiconError):
+
"""Raised when reference resolution fails."""
+
+
def __init__(
+
self, message: str, ref: Optional[str] = None, context: Optional[str] = None
+
):
+
self.ref = ref
+
self.context = context
+
+
details = []
+
if ref:
+
details.append(f"Reference: {ref}")
+
if context:
+
details.append(f"Context: {context}")
+
+
super().__init__(
+
f"Resolution error: {message}", "; ".join(details) if details else None
+
)
+
+
+
class GenerationError(LexiconError):
+
"""Raised when model generation fails."""
+
+
def __init__(
+
self,
+
message: str,
+
nsid: Optional[str] = None,
+
definition_type: Optional[str] = None,
+
):
+
self.nsid = nsid
+
self.definitionType = definition_type
+
+
details = []
+
if nsid:
+
details.append(f"NSID: {nsid}")
+
if definition_type:
+
details.append(f"Type: {definition_type}")
+
+
super().__init__(
+
f"Generation error: {message}", "; ".join(details) if details else None
+
)
+
+
+
class CompatibilityError(LexiconError):
+
"""Raised when compatibility checks fail."""
+
+
def __init__(
+
self,
+
message: str,
+
old_nsid: Optional[str] = None,
+
new_nsid: Optional[str] = None,
+
):
+
self.oldNsid = old_nsid
+
self.newNsid = new_nsid
+
+
details = []
+
if old_nsid:
+
details.append(f"Old NSID: {old_nsid}")
+
if new_nsid:
+
details.append(f"New NSID: {new_nsid}")
+
+
super().__init__(
+
f"Compatibility error: {message}", "; ".join(details) if details else None
+
)
+208
src/atpasser/lexicon/parser.py
···
···
+
"""Parser for ATProto Lexicon definitions."""
+
+
import json
+
from typing import Dict, Any, Optional, Type, Union
+
from pydantic import BaseModel, create_model
+
from .exceptions import ParseError, ValidationError
+
from .types import LexiconDocument, LexiconType
+
from .registry import registry
+
+
+
class LexiconParser:
+
"""Parser for ATProto Lexicon JSON definitions."""
+
+
def __init__(self):
+
self.validators = LexiconValidator()
+
+
def parse_document(self, json_data: Union[str, dict]) -> LexiconDocument:
+
"""Parse a Lexicon JSON document."""
+
try:
+
if isinstance(json_data, str):
+
data = json.loads(json_data)
+
else:
+
data = json_data
+
+
# Validate basic document structure
+
self.validators.validate_document_structure(data)
+
+
# Parse into Pydantic model
+
document = LexiconDocument.model_validate(data)
+
+
# Validate semantic rules
+
self.validators.validate_document_semantics(document)
+
+
return document
+
+
except Exception as e:
+
if isinstance(e, (ParseError, ValidationError)):
+
raise
+
raise ParseError(f"Failed to parse Lexicon document: {str(e)}")
+
+
def parse_and_register(self, json_data: Union[str, dict]) -> None:
+
"""Parse a Lexicon document and register it."""
+
document = self.parse_document(json_data)
+
registry.register_lexicon(document)
+
+
# Generate and register models for all definitions
+
generator = ModelGenerator()
+
for def_name, def_data in document.defs.items():
+
try:
+
model = generator.generate_model(document.id, def_name, def_data)
+
registry.register_model(document.id, model, def_name)
+
except Exception as e:
+
raise ParseError(
+
f"Failed to generate model for {def_name}: {str(e)}",
+
document.id,
+
def_name,
+
)
+
+
+
class LexiconValidator:
+
"""Validator for Lexicon documents."""
+
+
def validate_document_structure(self, data: Dict[str, Any]) -> None:
+
"""Validate basic document structure."""
+
required_fields = ["lexicon", "id", "defs"]
+
for field in required_fields:
+
if field not in data:
+
raise ValidationError(f"Missing required field: {field}")
+
+
if not isinstance(data["defs"], dict) or not data["defs"]:
+
raise ValidationError("defs must be a non-empty dictionary")
+
+
if data["lexicon"] != 1:
+
raise ValidationError("lexicon version must be 1")
+
+
def validate_document_semantics(self, document: LexiconDocument) -> None:
+
"""Validate semantic rules for Lexicon document."""
+
# Check primary type constraints
+
primary_types = {
+
LexiconType.RECORD,
+
LexiconType.QUERY,
+
LexiconType.PROCEDURE,
+
LexiconType.SUBSCRIPTION,
+
}
+
+
primary_defs = []
+
for def_name, def_data in document.defs.items():
+
def_type = def_data.get("type")
+
if def_type in primary_types:
+
primary_defs.append((def_name, def_type))
+
+
# Primary types should usually be named 'main'
+
if def_name != "main":
+
# This is a warning, not an error
+
pass
+
+
# Only one primary type allowed per document
+
if len(primary_defs) > 1:
+
raise ValidationError(
+
f"Multiple primary types found: {[name for name, _ in primary_defs]}",
+
document.id,
+
)
+
+
+
class ModelGenerator:
+
"""Generates Pydantic models from Lexicon definitions."""
+
+
def generate_model(
+
self, nsid: str, def_name: str, definition: Dict[str, Any]
+
) -> Type[BaseModel]:
+
"""Generate a Pydantic model from a Lexicon definition."""
+
def_type = definition.get("type")
+
+
if def_type == LexiconType.RECORD:
+
return self._generate_record_model(nsid, def_name, definition)
+
elif def_type == LexiconType.OBJECT:
+
return self._generate_object_model(nsid, def_name, definition)
+
elif def_type in [
+
LexiconType.QUERY,
+
LexiconType.PROCEDURE,
+
LexiconType.SUBSCRIPTION,
+
]:
+
return self._generate_primary_model(nsid, def_name, definition)
+
else:
+
# For simple types, create a basic model
+
return self._generate_simple_model(nsid, def_name, definition)
+
+
def _generate_record_model(
+
self, nsid: str, def_name: str, definition: Dict[str, Any]
+
) -> Type[BaseModel]:
+
"""Generate a model for record type."""
+
record_schema = definition.get("record", {})
+
return self._generate_object_model(nsid, def_name, record_schema)
+
+
def _generate_object_model(
+
self, nsid: str, def_name: str, definition: Dict[str, Any]
+
) -> Type[BaseModel]:
+
"""Generate a model for object type."""
+
properties = definition.get("properties", {})
+
required = definition.get("required", [])
+
+
field_definitions = {}
+
for prop_name, prop_schema in properties.items():
+
field_type = self._get_field_type(prop_schema)
+
field_definitions[prop_name] = (
+
field_type,
+
... if prop_name in required else None,
+
)
+
+
model_name = self._get_model_name(nsid, def_name)
+
return create_model(model_name, **field_definitions)
+
+
def _generate_primary_model(
+
self, nsid: str, def_name: str, definition: Dict[str, Any]
+
) -> Type[BaseModel]:
+
"""Generate a model for primary types (query, procedure, subscription)."""
+
# For now, create a basic model - specific handling can be added later
+
return self._generate_simple_model(nsid, def_name, definition)
+
+
def _generate_simple_model(
+
self, nsid: str, def_name: str, definition: Dict[str, Any]
+
) -> Type[BaseModel]:
+
"""Generate a simple model for basic types."""
+
field_type = self._get_field_type(definition)
+
model_name = self._get_model_name(nsid, def_name)
+
return create_model(model_name, value=(field_type, ...))
+
+
def _get_field_type(self, schema: Dict[str, Any]) -> Any:
+
"""Get the Python type for a schema definition."""
+
schema_type = schema.get("type")
+
+
type_mapping = {
+
LexiconType.NULL: type(None),
+
LexiconType.BOOLEAN: bool,
+
LexiconType.INTEGER: int,
+
LexiconType.STRING: str,
+
LexiconType.BYTES: bytes,
+
LexiconType.ARRAY: list,
+
LexiconType.OBJECT: dict,
+
}
+
+
if schema_type and schema_type in type_mapping:
+
return type_mapping[schema_type]
+
+
if schema_type == LexiconType.REF:
+
ref = schema.get("ref")
+
if ref:
+
return registry.resolve_ref(ref)
+
+
# Default to Any for complex types
+
return Any
+
+
def _get_model_name(self, nsid: str, def_name: str) -> str:
+
"""Generate a valid Python class name from NSID and definition name."""
+
# Convert NSID to PascalCase
+
parts = nsid.split(".")
+
name_parts = [part.capitalize() for part in parts]
+
+
# Add definition name
+
if def_name != "main":
+
def_part = def_name.capitalize()
+
name_parts.append(def_part)
+
+
return "".join(name_parts)
+
+
+
# Global parser instance
+
parser = LexiconParser()
+114
src/atpasser/lexicon/registry.py
···
···
+
"""Registry for managing Lexicon definitions and generated models."""
+
+
from typing import Dict, Optional, Type, Any
+
from pydantic import BaseModel
+
from .exceptions import ResolutionError
+
from .types import LexiconDocument
+
+
+
class LexiconRegistry:
+
"""Registry for storing and resolving Lexicon definitions and models."""
+
+
def __init__(self):
+
self._definitions: Dict[str, LexiconDocument] = {}
+
self._models: Dict[str, Type[BaseModel]] = {}
+
self._ref_cache: Dict[str, Type[BaseModel]] = {}
+
+
def register_lexicon(self, document: LexiconDocument) -> None:
+
"""Register a Lexicon document."""
+
nsid = document.id
+
if nsid in self._definitions:
+
raise ValueError(f"Lexicon with NSID {nsid} is already registered")
+
+
self._definitions[nsid] = document
+
+
# Clear cache for this NSID
+
self._clear_cache_for_nsid(nsid)
+
+
def get_lexicon(self, nsid: str) -> Optional[LexiconDocument]:
+
"""Get a registered Lexicon document by NSID."""
+
return self._definitions.get(nsid)
+
+
def register_model(
+
self, nsid: str, model: Type[BaseModel], definition_name: Optional[str] = None
+
) -> None:
+
"""Register a generated model for a Lexicon definition."""
+
key = self._get_model_key(nsid, definition_name)
+
self._models[key] = model
+
+
# Also cache for quick reference resolution
+
if definition_name and definition_name != "main":
+
ref_key = f"{nsid}#{definition_name}"
+
self._ref_cache[ref_key] = model
+
+
def get_model(
+
self, nsid: str, definition_name: Optional[str] = None
+
) -> Optional[Type[BaseModel]]:
+
"""Get a registered model by NSID and optional definition name."""
+
key = self._get_model_key(nsid, definition_name)
+
return self._models.get(key)
+
+
def resolve_ref(self, ref: str) -> Type[BaseModel]:
+
"""Resolve a reference to a model."""
+
if ref in self._ref_cache:
+
return self._ref_cache[ref]
+
+
# Parse the reference
+
if "#" in ref:
+
nsid, definition_name = ref.split("#", 1)
+
else:
+
nsid, definition_name = ref, "main"
+
+
model = self.get_model(nsid, definition_name)
+
if model is None:
+
raise ResolutionError(f"Reference not found: {ref}", ref)
+
+
# Cache for future use
+
self._ref_cache[ref] = model
+
return model
+
+
def has_lexicon(self, nsid: str) -> bool:
+
"""Check if a Lexicon is registered."""
+
return nsid in self._definitions
+
+
def has_model(self, nsid: str, definition_name: Optional[str] = None) -> bool:
+
"""Check if a model is registered."""
+
key = self._get_model_key(nsid, definition_name)
+
return key in self._models
+
+
def clear_cache(self) -> None:
+
"""Clear all cached models and references."""
+
self._models.clear()
+
self._ref_cache.clear()
+
+
def _get_model_key(self, nsid: str, definition_name: Optional[str]) -> str:
+
"""Get the internal key for model storage."""
+
if definition_name:
+
return f"{nsid}#{definition_name}"
+
return f"{nsid}#main"
+
+
def _clear_cache_for_nsid(self, nsid: str) -> None:
+
"""Clear cache entries for a specific NSID."""
+
# Clear models
+
keys_to_remove = [
+
key for key in self._models.keys() if key.startswith(f"{nsid}#")
+
]
+
for key in keys_to_remove:
+
del self._models[key]
+
+
# Clear ref cache
+
keys_to_remove = [key for key in self._ref_cache.keys() if key.startswith(nsid)]
+
for key in keys_to_remove:
+
del self._ref_cache[key]
+
+
def list_lexicons(self) -> Dict[str, LexiconDocument]:
+
"""List all registered Lexicon documents."""
+
return self._definitions.copy()
+
+
def list_models(self) -> Dict[str, Type[BaseModel]]:
+
"""List all registered models."""
+
return self._models.copy()
+
+
+
# Global registry instance
+
registry = LexiconRegistry()
+155
src/atpasser/lexicon/types.py
···
···
+
"""Type definitions for ATProto Lexicon module."""
+
+
from typing import Dict, List, Optional, Union, Any, Type
+
from enum import Enum
+
from pydantic import BaseModel, Field
+
+
+
class LexiconType(str, Enum):
+
"""Enumeration of Lexicon definition types."""
+
+
NULL = "null"
+
BOOLEAN = "boolean"
+
INTEGER = "integer"
+
STRING = "string"
+
BYTES = "bytes"
+
CID_LINK = "cid-link"
+
BLOB = "blob"
+
ARRAY = "array"
+
OBJECT = "object"
+
PARAMS = "params"
+
TOKEN = "token"
+
REF = "ref"
+
UNION = "union"
+
UNKNOWN = "unknown"
+
RECORD = "record"
+
QUERY = "query"
+
PROCEDURE = "procedure"
+
SUBSCRIPTION = "subscription"
+
+
+
class LexiconDefinition(BaseModel):
+
"""Base class for Lexicon definitions."""
+
+
type: LexiconType
+
description: Optional[str] = None
+
+
+
class IntegerConstraints(BaseModel):
+
"""Constraints for integer fields."""
+
+
minimum: Optional[int] = None
+
maximum: Optional[int] = None
+
enum: Optional[List[int]] = None
+
default: Optional[int] = None
+
const: Optional[int] = None
+
+
+
class StringConstraints(BaseModel):
+
"""Constraints for string fields."""
+
+
format: Optional[str] = None
+
maxLength: Optional[int] = None
+
minLength: Optional[int] = None
+
maxGraphemes: Optional[int] = None
+
minGraphemes: Optional[int] = None
+
knownValues: Optional[List[str]] = None
+
enum: Optional[List[str]] = None
+
default: Optional[str] = None
+
const: Optional[str] = None
+
+
+
class ArrayConstraints(BaseModel):
+
"""Constraints for array fields."""
+
+
items: Dict[str, Any] # Schema definition for array items
+
minLength: Optional[int] = None
+
maxLength: Optional[int] = None
+
+
+
class ObjectConstraints(BaseModel):
+
"""Constraints for object fields."""
+
+
properties: Dict[str, Dict[str, Any]] # Map of property names to schemas
+
required: Optional[List[str]] = None
+
nullable: Optional[List[str]] = None
+
+
+
class BlobConstraints(BaseModel):
+
"""Constraints for blob fields."""
+
+
accept: Optional[List[str]] = None # MIME types
+
maxSize: Optional[int] = None # Maximum size in bytes
+
+
+
class ParamsConstraints(BaseModel):
+
"""Constraints for params fields."""
+
+
properties: Dict[str, Dict[str, Any]]
+
required: Optional[List[str]] = None
+
+
+
class RefDefinition(BaseModel):
+
"""Reference definition."""
+
+
ref: str # Reference to another schema
+
+
+
class UnionDefinition(BaseModel):
+
"""Union type definition."""
+
+
refs: List[str] # List of references
+
closed: Optional[bool] = False # Whether union is closed
+
+
+
class RecordDefinition(LexiconDefinition):
+
"""Record type definition."""
+
+
key: str # Record key type
+
record: Dict[str, Any] # Object schema
+
+
+
class QueryDefinition(LexiconDefinition):
+
"""Query type definition."""
+
+
parameters: Optional[Dict[str, Any]] = None # Params schema
+
output: Optional[Dict[str, Any]] = None # Output schema
+
+
+
class ProcedureDefinition(LexiconDefinition):
+
"""Procedure type definition."""
+
+
parameters: Optional[Dict[str, Any]] = None # Params schema
+
input: Optional[Dict[str, Any]] = None # Input schema
+
output: Optional[Dict[str, Any]] = None # Output schema
+
errors: Optional[List[Dict[str, Any]]] = None # Error definitions
+
+
+
class SubscriptionDefinition(LexiconDefinition):
+
"""Subscription type definition."""
+
+
parameters: Optional[Dict[str, Any]] = None # Params schema
+
message: Optional[Dict[str, Any]] = None # Message schema
+
errors: Optional[List[Dict[str, Any]]] = None # Error definitions
+
+
+
class LexiconDocument(BaseModel):
+
"""Complete Lexicon document."""
+
+
lexicon: int # Lexicon version (always 1)
+
id: str # NSID of the Lexicon
+
description: Optional[str] = None
+
defs: Dict[str, Dict[str, Any]] # Map of definition names to schemas
+
+
+
class ErrorDefinition(BaseModel):
+
"""Error definition for procedures and subscriptions."""
+
+
name: str # Error name
+
description: Optional[str] = None
+
+
+
# Type aliases for convenience
+
LexiconSchema = Dict[str, Any]
+
PropertyMap = Dict[str, LexiconSchema]
+
DefinitionMap = Dict[str, Union[LexiconDefinition, Dict[str, Any]]]