···1314class JsonDecoder(json.JSONDecoder):
15 """A JSON decoder that supports ATProto data types.
16-17 This decoder extends the standard JSON decoder to handle ATProto-specific
18 data types, including bytes, CID links, and typed objects.
19-20 Attributes:
21 type_hook_registry: Registry for type-specific hooks.
22 encoding: The encoding to use for string deserialization.
23 """
24-25 def __init__(
26 self,
27 *,
···29 type_hook_registry: Optional[Any] = None,
30 type_processor_registry: Optional[Any] = None,
31 encoding: str = "utf-8",
32- **kwargs: Any
33 ) -> None:
34 """Initialize the JSON decoder.
35-36 Args:
37 object_hook: Optional function to call with each decoded object.
38 type_hook_registry: Registry for type-specific hooks.
···45 type_hook_registry = type_processor_registry.to_hook_registry()
46 elif type_hook_registry is None:
47 from .hooks import get_global_registry
048 type_hook_registry = get_global_registry()
49-50 # Create a combined object hook that calls both the custom hook and our hook
51 combined_hook = self._create_combined_hook(object_hook, type_hook_registry)
52-53 super().__init__(object_hook=combined_hook, **kwargs)
54 self.type_hook_registry = type_hook_registry
55 self.type_processor_registry = type_processor_registry
56 self.encoding = encoding
57-58 def _create_combined_hook(
59 self,
60 custom_hook: Optional[Callable[[Dict[str, Any]], Any]],
61- type_hook_registry: Optional[Any]
62 ) -> Callable[[Dict[str, Any]], Any]:
63 """Create a combined object hook function.
64-65 Args:
66 custom_hook: Optional custom object hook function.
67 type_hook_registry: Registry for type-specific hooks.
68-69 Returns:
70 A combined object hook function.
71 """
072 def combined_hook(obj: Dict[str, Any]) -> Any:
73 # First, apply our ATProto-specific decoding
74 decoded_obj = self._atproto_object_hook(obj)
75-76 # Then, apply the custom hook if provided
77 if custom_hook is not None:
78 decoded_obj = custom_hook(decoded_obj)
79-80 return decoded_obj
81-82 return combined_hook
83-84 def _atproto_object_hook(self, obj: Dict[str, Any]) -> Any:
85 """Handle ATProto-specific object decoding.
86-87 Args:
88 obj: The object to decode.
89-90 Returns:
91 The decoded object.
92 """
···96 # If there are other keys, this is invalid
97 raise ValueError(f"Invalid $bytes object: {obj}")
98 return base64.b64decode(obj["$bytes"].encode(self.encoding))
99-100 # Handle $link key (CID parsing)
101 elif "$link" in obj:
102 if len(obj) != 1:
103 # If there are other keys, this is invalid
104 raise ValueError(f"Invalid $link object: {obj}")
105 return make_cid(obj["$link"])
106-107 # Handle $type key (typed objects)
108 elif "$type" in obj:
109 type_value = obj["$type"]
110 remaining_obj = {k: v for k, v in obj.items() if k != "$type"}
111-112 # Check if there's a registered type handler
113 if self.type_hook_registry is not None:
114 handler = self.type_hook_registry.get_handler(type_value)
115 if handler is not None:
116 return handler(remaining_obj)
117-118 # If no handler is registered, return a typed object
119 return TypedObject(type_value, remaining_obj)
120-121 # Handle nested objects recursively
122 elif isinstance(obj, dict):
123- return {k: self._atproto_object_hook(v) if isinstance(v, dict) else v
124- for k, v in obj.items()}
125-00126 return obj
127128129class TypedObject:
130 """A typed object in the ATProto data model.
131-132 This class represents an object with a $type field in the ATProto data model.
133-134 Attributes:
135 type: The type of the object.
136 data: The data associated with the object.
137 """
138-139 def __init__(self, type_name: str, data: Dict[str, Any]) -> None:
140 """Initialize a typed object.
141-142 Args:
143 type_name: The type of the object.
144 data: The data associated with the object.
145 """
146 self.type_name = type_name
147 self.data = data
148-149 def __repr__(self) -> str:
150 """Return a string representation of the typed object.
151-152 Returns:
153 A string representation of the typed object.
154 """
155 return f"TypedObject(type_name={self.type_name!r}, data={self.data!r})"
156-157 def __eq__(self, other: Any) -> bool:
158 """Check if two typed objects are equal.
159-160 Args:
161 other: The object to compare with.
162-163 Returns:
164 True if the objects are equal, False otherwise.
165 """
166 if not isinstance(other, TypedObject):
167 return False
168 return self.type_name == other.type_name and self.data == other.data
169-170 def __atproto_json_encode__(self) -> Dict[str, Any]:
171 """Encode the typed object to a JSON-serializable format.
172-173 Returns:
174 A JSON-serializable representation of the typed object.
175 """
176 result = {"$type": self.type_name}
177 result.update(self.data)
178- return result
···1314class JsonDecoder(json.JSONDecoder):
15 """A JSON decoder that supports ATProto data types.
16+17 This decoder extends the standard JSON decoder to handle ATProto-specific
18 data types, including bytes, CID links, and typed objects.
19+20 Attributes:
21 type_hook_registry: Registry for type-specific hooks.
22 encoding: The encoding to use for string deserialization.
23 """
24+25 def __init__(
26 self,
27 *,
···29 type_hook_registry: Optional[Any] = None,
30 type_processor_registry: Optional[Any] = None,
31 encoding: str = "utf-8",
32+ **kwargs: Any,
33 ) -> None:
34 """Initialize the JSON decoder.
35+36 Args:
37 object_hook: Optional function to call with each decoded object.
38 type_hook_registry: Registry for type-specific hooks.
···45 type_hook_registry = type_processor_registry.to_hook_registry()
46 elif type_hook_registry is None:
47 from .hooks import get_global_registry
48+49 type_hook_registry = get_global_registry()
50+51 # Create a combined object hook that calls both the custom hook and our hook
52 combined_hook = self._create_combined_hook(object_hook, type_hook_registry)
53+54 super().__init__(object_hook=combined_hook, **kwargs)
55 self.type_hook_registry = type_hook_registry
56 self.type_processor_registry = type_processor_registry
57 self.encoding = encoding
58+59 def _create_combined_hook(
60 self,
61 custom_hook: Optional[Callable[[Dict[str, Any]], Any]],
62+ type_hook_registry: Optional[Any],
63 ) -> Callable[[Dict[str, Any]], Any]:
64 """Create a combined object hook function.
65+66 Args:
67 custom_hook: Optional custom object hook function.
68 type_hook_registry: Registry for type-specific hooks.
69+70 Returns:
71 A combined object hook function.
72 """
73+74 def combined_hook(obj: Dict[str, Any]) -> Any:
75 # First, apply our ATProto-specific decoding
76 decoded_obj = self._atproto_object_hook(obj)
77+78 # Then, apply the custom hook if provided
79 if custom_hook is not None:
80 decoded_obj = custom_hook(decoded_obj)
81+82 return decoded_obj
83+84 return combined_hook
85+86 def _atproto_object_hook(self, obj: Dict[str, Any]) -> Any:
87 """Handle ATProto-specific object decoding.
88+89 Args:
90 obj: The object to decode.
91+92 Returns:
93 The decoded object.
94 """
···98 # If there are other keys, this is invalid
99 raise ValueError(f"Invalid $bytes object: {obj}")
100 return base64.b64decode(obj["$bytes"].encode(self.encoding))
101+102 # Handle $link key (CID parsing)
103 elif "$link" in obj:
104 if len(obj) != 1:
105 # If there are other keys, this is invalid
106 raise ValueError(f"Invalid $link object: {obj}")
107 return make_cid(obj["$link"])
108+109 # Handle $type key (typed objects)
110 elif "$type" in obj:
111 type_value = obj["$type"]
112 remaining_obj = {k: v for k, v in obj.items() if k != "$type"}
113+114 # Check if there's a registered type handler
115 if self.type_hook_registry is not None:
116 handler = self.type_hook_registry.get_handler(type_value)
117 if handler is not None:
118 return handler(remaining_obj)
119+120 # If no handler is registered, return a typed object
121 return TypedObject(type_value, remaining_obj)
122+123 # Handle nested objects recursively
124 elif isinstance(obj, dict):
125+ return {
126+ k: self._atproto_object_hook(v) if isinstance(v, dict) else v
127+ for k, v in obj.items()
128+ }
129+130 return obj
131132133class TypedObject:
134 """A typed object in the ATProto data model.
135+136 This class represents an object with a $type field in the ATProto data model.
137+138 Attributes:
139 type: The type of the object.
140 data: The data associated with the object.
141 """
142+143 def __init__(self, type_name: str, data: Dict[str, Any]) -> None:
144 """Initialize a typed object.
145+146 Args:
147 type_name: The type of the object.
148 data: The data associated with the object.
149 """
150 self.type_name = type_name
151 self.data = data
152+153 def __repr__(self) -> str:
154 """Return a string representation of the typed object.
155+156 Returns:
157 A string representation of the typed object.
158 """
159 return f"TypedObject(type_name={self.type_name!r}, data={self.data!r})"
160+161 def __eq__(self, other: Any) -> bool:
162 """Check if two typed objects are equal.
163+164 Args:
165 other: The object to compare with.
166+167 Returns:
168 True if the objects are equal, False otherwise.
169 """
170 if not isinstance(other, TypedObject):
171 return False
172 return self.type_name == other.type_name and self.data == other.data
173+174 def __atproto_json_encode__(self) -> Dict[str, Any]:
175 """Encode the typed object to a JSON-serializable format.
176+177 Returns:
178 A JSON-serializable representation of the typed object.
179 """
180 result = {"$type": self.type_name}
181 result.update(self.data)
182+ return result
+10-10
src/atpasser/data/encoder.py
···1314class JsonEncoder(json.JSONEncoder):
15 """A JSON encoder that supports ATProto data types.
16-17 This encoder extends the standard JSON encoder to handle ATProto-specific
18 data types, including bytes, CID links, and typed objects.
19-20 Attributes:
21 encoding (str): The encoding to use for string serialization.
22 type_processor_registry: Registry for type-specific processors.
23 """
24-25 def __init__(
26 self,
27 *,
28 encoding: str = "utf-8",
29 type_processor_registry: Optional[Any] = None,
30- **kwargs: Any
31 ) -> None:
32 """Initialize the JSON encoder.
33-34 Args:
35 encoding: The encoding to use for string serialization.
36 type_processor_registry: Registry for type-specific processors.
···39 super().__init__(**kwargs)
40 self.encoding = encoding
41 self.type_processor_registry = type_processor_registry
42-43 def default(self, o: Any) -> Any:
44 """Convert an object to a serializable format.
45-46 Args:
47 o: The object to serialize.
48-49 Returns:
50 A serializable representation of the object.
51-52 Raises:
53 TypeError: If the object is not serializable.
54 """
···79 return [self.default(item) for item in o]
80 else:
81 # Use the parent class for other types
82- return super().default(o)
···1314class JsonEncoder(json.JSONEncoder):
15 """A JSON encoder that supports ATProto data types.
16+17 This encoder extends the standard JSON encoder to handle ATProto-specific
18 data types, including bytes, CID links, and typed objects.
19+20 Attributes:
21 encoding (str): The encoding to use for string serialization.
22 type_processor_registry: Registry for type-specific processors.
23 """
24+25 def __init__(
26 self,
27 *,
28 encoding: str = "utf-8",
29 type_processor_registry: Optional[Any] = None,
30+ **kwargs: Any,
31 ) -> None:
32 """Initialize the JSON encoder.
33+34 Args:
35 encoding: The encoding to use for string serialization.
36 type_processor_registry: Registry for type-specific processors.
···39 super().__init__(**kwargs)
40 self.encoding = encoding
41 self.type_processor_registry = type_processor_registry
42+43 def default(self, o: Any) -> Any:
44 """Convert an object to a serializable format.
45+46 Args:
47 o: The object to serialize.
48+49 Returns:
50 A serializable representation of the object.
51+52 Raises:
53 TypeError: If the object is not serializable.
54 """
···79 return [self.default(item) for item in o]
80 else:
81 # Use the parent class for other types
82+ return super().default(o)
+51-46
src/atpasser/data/hooks.py
···9from typing import Any, Callable, Dict, Optional, TypeVar, Union
1011# Type variable for the decorated function
12-F = TypeVar('F', bound=Callable[..., Any])
131415class TypeHookRegistry:
16 """Registry for type-specific hooks in the ATProto JSON decoder.
17-18 This class maintains a registry of type-specific hooks that can be used
19 to customize the decoding of objects with $type keys in the ATProto data model.
20-21 Attributes:
22 _handlers: Dictionary mapping type names to handler functions.
23 """
24-25 def __init__(self) -> None:
26 """Initialize the type hook registry."""
27 self._handlers: Dict[str, Callable[[Dict[str, Any]], Any]] = {}
28-29 def register(self, type_name: str) -> Callable[[F], F]:
30 """Register a type handler function.
31-32 This method can be used as a decorator to register a function as a handler
33 for a specific type.
34-35 Args:
36 type_name: The name of the type to handle.
37-38 Returns:
39 A decorator function that registers the decorated function as a handler.
40-41 Example:
42 >>> registry = TypeHookRegistry()
43- >>>
44 >>> @registry.register("app.bsky.feed.post")
45 ... def handle_post(data: Dict[str, Any]) -> Any:
46 ... return Post(**data)
47 """
048 def decorator(func: F) -> F:
49 self._handlers[type_name] = func
50 return func
51-52 return decorator
53-54- def register_handler(self, type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None:
0055 """Register a type handler function directly.
56-57 Args:
58 type_name: The name of the type to handle.
59 handler: The function to call when decoding objects of this type.
60-61 Example:
62 >>> registry = TypeHookRegistry()
63- >>>
64 >>> def handle_post(data: Dict[str, Any]) -> Any:
65 ... return Post(**data)
66- >>>
67 >>> registry.register_handler("app.bsky.feed.post", handle_post)
68 """
69 self._handlers[type_name] = handler
70-71 def unregister(self, type_name: str) -> None:
72 """Unregister a type handler function.
73-74 Args:
75 type_name: The name of the type to unregister.
76 """
77 if type_name in self._handlers:
78 del self._handlers[type_name]
79-80 def get_handler(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
81 """Get the handler function for a specific type.
82-83 Args:
84 type_name: The name of the type to get the handler for.
85-86 Returns:
87 The handler function for the specified type, or None if no handler
88 is registered.
89 """
90 return self._handlers.get(type_name)
91-92 def has_handler(self, type_name: str) -> bool:
93 """Check if a handler is registered for a specific type.
94-95 Args:
96 type_name: The name of the type to check.
97-98 Returns:
99 True if a handler is registered for the specified type, False otherwise.
100 """
101 return type_name in self._handlers
102-103 def clear(self) -> None:
104 """Clear all registered handlers."""
105 self._handlers.clear()
106-107 def get_registered_types(self) -> set:
108 """Get the set of all registered type names.
109-110 Returns:
111 A set of all registered type names.
112 """
···119120def type_handler(type_name: str) -> Callable[[F], F]:
121 """Register a global type handler function.
122-123 This decorator registers a function as a global handler for a specific type
124 in the ATProto data model.
125-126 Args:
127 type_name: The name of the type to handle.
128-129 Returns:
130 A decorator function that registers the decorated function as a handler.
131-132 Example:
133 >>> @type_handler("app.bsky.feed.post")
134 ... def handle_post(data: Dict[str, Any]) -> Any:
···139140def get_global_registry() -> TypeHookRegistry:
141 """Get the global type hook registry.
142-143 Returns:
144 The global TypeHookRegistry instance.
145 """
146 return _global_registry
147148149-def register_type_handler(type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None:
00150 """Register a global type handler function directly.
151-152 Args:
153 type_name: The name of the type to handle.
154 handler: The function to call when decoding objects of this type.
155-156 Example:
157 >>> def handle_post(data: Dict[str, Any]) -> Any:
158 ... return Post(**data)
159- >>>
160 >>> register_type_handler("app.bsky.feed.post", handle_post)
161 """
162 _global_registry.register_handler(type_name, handler)
···164165def unregister_type_handler(type_name: str) -> None:
166 """Unregister a global type handler function.
167-168 Args:
169 type_name: The name of the type to unregister.
170 """
···173174def get_type_handler(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
175 """Get the global handler function for a specific type.
176-177 Args:
178 type_name: The name of the type to get the handler for.
179-180 Returns:
181 The handler function for the specified type, or None if no handler
182 is registered.
···186187def has_type_handler(type_name: str) -> bool:
188 """Check if a global handler is registered for a specific type.
189-190 Args:
191 type_name: The name of the type to check.
192-193 Returns:
194 True if a handler is registered for the specified type, False otherwise.
195 """
···203204def get_registered_types() -> set:
205 """Get the set of all globally registered type names.
206-207 Returns:
208 A set of all registered type names.
209 """
···212213def create_registry() -> TypeHookRegistry:
214 """Create a new type hook registry.
215-216 This function creates a new, independent registry that can be used
217 instead of the global registry.
218-219 Returns:
220 A new TypeHookRegistry instance.
221 """
222- return TypeHookRegistry()
···9from typing import Any, Callable, Dict, Optional, TypeVar, Union
1011# Type variable for the decorated function
12+F = TypeVar("F", bound=Callable[..., Any])
131415class TypeHookRegistry:
16 """Registry for type-specific hooks in the ATProto JSON decoder.
17+18 This class maintains a registry of type-specific hooks that can be used
19 to customize the decoding of objects with $type keys in the ATProto data model.
20+21 Attributes:
22 _handlers: Dictionary mapping type names to handler functions.
23 """
24+25 def __init__(self) -> None:
26 """Initialize the type hook registry."""
27 self._handlers: Dict[str, Callable[[Dict[str, Any]], Any]] = {}
28+29 def register(self, type_name: str) -> Callable[[F], F]:
30 """Register a type handler function.
31+32 This method can be used as a decorator to register a function as a handler
33 for a specific type.
34+35 Args:
36 type_name: The name of the type to handle.
37+38 Returns:
39 A decorator function that registers the decorated function as a handler.
40+41 Example:
42 >>> registry = TypeHookRegistry()
43+ >>>
44 >>> @registry.register("app.bsky.feed.post")
45 ... def handle_post(data: Dict[str, Any]) -> Any:
46 ... return Post(**data)
47 """
48+49 def decorator(func: F) -> F:
50 self._handlers[type_name] = func
51 return func
52+53 return decorator
54+55+ def register_handler(
56+ self, type_name: str, handler: Callable[[Dict[str, Any]], Any]
57+ ) -> None:
58 """Register a type handler function directly.
59+60 Args:
61 type_name: The name of the type to handle.
62 handler: The function to call when decoding objects of this type.
63+64 Example:
65 >>> registry = TypeHookRegistry()
66+ >>>
67 >>> def handle_post(data: Dict[str, Any]) -> Any:
68 ... return Post(**data)
69+ >>>
70 >>> registry.register_handler("app.bsky.feed.post", handle_post)
71 """
72 self._handlers[type_name] = handler
73+74 def unregister(self, type_name: str) -> None:
75 """Unregister a type handler function.
76+77 Args:
78 type_name: The name of the type to unregister.
79 """
80 if type_name in self._handlers:
81 del self._handlers[type_name]
82+83 def get_handler(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
84 """Get the handler function for a specific type.
85+86 Args:
87 type_name: The name of the type to get the handler for.
88+89 Returns:
90 The handler function for the specified type, or None if no handler
91 is registered.
92 """
93 return self._handlers.get(type_name)
94+95 def has_handler(self, type_name: str) -> bool:
96 """Check if a handler is registered for a specific type.
97+98 Args:
99 type_name: The name of the type to check.
100+101 Returns:
102 True if a handler is registered for the specified type, False otherwise.
103 """
104 return type_name in self._handlers
105+106 def clear(self) -> None:
107 """Clear all registered handlers."""
108 self._handlers.clear()
109+110 def get_registered_types(self) -> set:
111 """Get the set of all registered type names.
112+113 Returns:
114 A set of all registered type names.
115 """
···122123def type_handler(type_name: str) -> Callable[[F], F]:
124 """Register a global type handler function.
125+126 This decorator registers a function as a global handler for a specific type
127 in the ATProto data model.
128+129 Args:
130 type_name: The name of the type to handle.
131+132 Returns:
133 A decorator function that registers the decorated function as a handler.
134+135 Example:
136 >>> @type_handler("app.bsky.feed.post")
137 ... def handle_post(data: Dict[str, Any]) -> Any:
···142143def get_global_registry() -> TypeHookRegistry:
144 """Get the global type hook registry.
145+146 Returns:
147 The global TypeHookRegistry instance.
148 """
149 return _global_registry
150151152+def register_type_handler(
153+ type_name: str, handler: Callable[[Dict[str, Any]], Any]
154+) -> None:
155 """Register a global type handler function directly.
156+157 Args:
158 type_name: The name of the type to handle.
159 handler: The function to call when decoding objects of this type.
160+161 Example:
162 >>> def handle_post(data: Dict[str, Any]) -> Any:
163 ... return Post(**data)
164+ >>>
165 >>> register_type_handler("app.bsky.feed.post", handle_post)
166 """
167 _global_registry.register_handler(type_name, handler)
···169170def unregister_type_handler(type_name: str) -> None:
171 """Unregister a global type handler function.
172+173 Args:
174 type_name: The name of the type to unregister.
175 """
···178179def get_type_handler(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
180 """Get the global handler function for a specific type.
181+182 Args:
183 type_name: The name of the type to get the handler for.
184+185 Returns:
186 The handler function for the specified type, or None if no handler
187 is registered.
···191192def has_type_handler(type_name: str) -> bool:
193 """Check if a global handler is registered for a specific type.
194+195 Args:
196 type_name: The name of the type to check.
197+198 Returns:
199 True if a handler is registered for the specified type, False otherwise.
200 """
···208209def get_registered_types() -> set:
210 """Get the set of all globally registered type names.
211+212 Returns:
213 A set of all registered type names.
214 """
···217218def create_registry() -> TypeHookRegistry:
219 """Create a new type hook registry.
220+221 This function creates a new, independent registry that can be used
222 instead of the global registry.
223+224 Returns:
225 A new TypeHookRegistry instance.
226 """
227+ return TypeHookRegistry()
+120-114
src/atpasser/data/types.py
···10from .hooks import TypeHookRegistry
1112# Type variable for the decorated class
13-T = TypeVar('T')
141516class TypeProcessor:
17 """A type processor for ATProto JSON objects.
18-19 This class represents a processor for a specific type in the ATProto data model.
20 It contains information about how to convert JSON data to Python objects and
21 vice versa.
22-23 Attributes:
24 type_name: The name of the type this processor handles.
25 decoder: The function to decode JSON data to a Python object.
26 encoder: The function to encode a Python object to JSON data.
27 priority: The priority of this processor (higher values = higher priority).
28 """
29-30 def __init__(
31 self,
32 type_name: str,
33 decoder: Optional[Callable[[Dict[str, Any]], Any]] = None,
34 encoder: Optional[Callable[[Any], Dict[str, Any]]] = None,
35- priority: int = 0
36 ) -> None:
37 """Initialize a type processor.
38-39 Args:
40 type_name: The name of the type this processor handles.
41 decoder: The function to decode JSON data to a Python object.
···46 self.decoder = decoder
47 self.encoder = encoder
48 self.priority = priority
49-50 def decode(self, data: Dict[str, Any]) -> Any:
51 """Decode JSON data to a Python object.
52-53 Args:
54 data: The JSON data to decode.
55-56 Returns:
57 The decoded Python object.
58-59 Raises:
60 ValueError: If no decoder is registered.
61 """
62 if self.decoder is None:
63 raise ValueError(f"No decoder registered for type {self.type_name}")
64 return self.decoder(data)
65-66 def encode(self, obj: Any) -> Dict[str, Any]:
67 """Encode a Python object to JSON data.
68-69 Args:
70 obj: The Python object to encode.
71-72 Returns:
73 The encoded JSON data.
74-75 Raises:
76 ValueError: If no encoder is registered.
77 """
···8283class TypeProcessorRegistry:
84 """Registry for type processors in the ATProto JSON decoder.
85-86 This class maintains a registry of type processors that can be used
87 to customize the encoding and decoding of objects with $type keys in
88 the ATProto data model.
89-90 Attributes:
91 _processors: Dictionary mapping type names to processor lists.
92 """
93-94 def __init__(self) -> None:
95 """Initialize the type processor registry."""
96 self._processors: Dict[str, List[TypeProcessor]] = {}
97-98 def register_processor(self, processor: TypeProcessor) -> None:
99 """Register a type processor.
100-101 Args:
102 processor: The type processor to register.
103 """
104 if processor.type_name not in self._processors:
105 self._processors[processor.type_name] = []
106-107 self._processors[processor.type_name].append(processor)
108 # Sort processors by priority (descending)
109- self._processors[processor.type_name].sort(key=lambda p: p.priority, reverse=True)
110-00111 def register(
112- self,
113- type_name: str,
114- priority: int = 0
115 ) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
116 """Register a type decoder function.
117-118 This method can be used as a decorator to register a function as a decoder
119 for a specific type.
120-121 Args:
122 type_name: The name of the type to handle.
123 priority: The priority of this processor (higher values = higher priority).
124-125 Returns:
126 A decorator function that registers the decorated function as a decoder.
127-128 Example:
129 >>> registry = TypeProcessorRegistry()
130- >>>
131 >>> @registry.register("app.bsky.feed.post", priority=10)
132 ... def decode_post(data: Dict[str, Any]) -> Any:
133 ... return Post(**data)
134 """
135- def decorator(func: Callable[[Dict[str, Any]], Any]) -> Callable[[Dict[str, Any]], Any]:
000136 processor = TypeProcessor(type_name, decoder=func, priority=priority)
137 self.register_processor(processor)
138 return func
139-140 return decorator
141-142 def register_encoder(
143- self,
144- type_name: str,
145- priority: int = 0
146 ) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
147 """Register a type encoder function.
148-149 This method can be used as a decorator to register a function as an encoder
150 for a specific type.
151-152 Args:
153 type_name: The name of the type to handle.
154 priority: The priority of this processor (higher values = higher priority).
155-156 Returns:
157 A decorator function that registers the decorated function as an encoder.
158-159 Example:
160 >>> registry = TypeProcessorRegistry()
161- >>>
162 >>> @registry.register_encoder("app.bsky.feed.post", priority=10)
163 ... def encode_post(post: Post) -> Dict[str, Any]:
164 ... return {"text": post.text, "createdAt": post.created_at}
165 """
166- def decorator(func: Callable[[Any], Dict[str, Any]]) -> Callable[[Any], Dict[str, Any]]:
000167 # Check if a processor for this type already exists
168 if type_name in self._processors:
169 for processor in self._processors[type_name]:
···173 break
174 else:
175 # No decoder found, create a new processor
176- processor = TypeProcessor(type_name, encoder=func, priority=priority)
00177 self.register_processor(processor)
178 else:
179 # No processor exists, create a new one
180 processor = TypeProcessor(type_name, encoder=func, priority=priority)
181 self.register_processor(processor)
182-183 return func
184-185 return decorator
186-187 def register_class(
188- self,
189- type_name: str,
190- priority: int = 0
191 ) -> Callable[[Type[T]], Type[T]]:
192 """Register a class for both encoding and decoding.
193-194 This method can be used as a decorator to register a class for both
195 encoding and decoding of a specific type.
196-197 The class must have a class method `from_json` that takes a dictionary
198 and returns an instance of the class, and an instance method `to_json`
199 that returns a dictionary.
200-201 Args:
202 type_name: The name of the type to handle.
203 priority: The priority of this processor (higher values = higher priority).
204-205 Returns:
206 A decorator function that registers the decorated class.
207-208 Example:
209 >>> registry = TypeProcessorRegistry()
210- >>>
211 >>> @registry.register_class("app.bsky.feed.post", priority=10)
212 ... class Post:
213 ... def __init__(self, text: str, created_at: str) -> None:
214 ... self.text = text
215 ... self.created_at = created_at
216- ...
217 ... @classmethod
218 ... def from_json(cls, data: Dict[str, Any]) -> "Post":
219 ... return cls(data["text"], data["createdAt"])
220- ...
221 ... def to_json(self) -> Dict[str, Any]:
222 ... return {"text": self.text, "createdAt": self.created_at}
223 """
0224 def decorator(cls: Type[T]) -> Type[T]:
225 # Create decoder from class method
226 if hasattr(cls, "from_json"):
···232 # Create a decoder that passes the data as keyword arguments
233 decoder = lambda data: cls(**data)
234 else:
235- raise ValueError(f"Class {cls.__name__} has no from_json method or compatible __init__")
236-00237 # Create encoder from instance method
238 if hasattr(cls, "to_json"):
239 encoder = lambda obj: obj.to_json()
240 else:
241 raise ValueError(f"Class {cls.__name__} has no to_json method")
242-243 # Register the processor
244- processor = TypeProcessor(type_name, decoder=decoder, encoder=encoder, priority=priority)
00245 self.register_processor(processor)
246-247 return cls
248-249 return decorator
250-251 def unregister(self, type_name: str, priority: Optional[int] = None) -> None:
252 """Unregister type processors.
253-254 Args:
255 type_name: The name of the type to unregister.
256 priority: If specified, only unregister processors with this priority.
···264 else:
265 # Remove all processors for this type
266 del self._processors[type_name]
267-268 def get_decoder(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
269 """Get the decoder function for a specific type.
270-271 Args:
272 type_name: The name of the type to get the decoder for.
273-274 Returns:
275 The decoder function for the specified type, or None if no decoder
276 is registered.
···279 # Return the decoder of the highest priority processor
280 return self._processors[type_name][0].decoder
281 return None
282-283 def get_encoder(self, type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
284 """Get the encoder function for a specific type.
285-286 Args:
287 type_name: The name of the type to get the encoder for.
288-289 Returns:
290 The encoder function for the specified type, or None if no encoder
291 is registered.
···294 # Return the encoder of the highest priority processor
295 return self._processors[type_name][0].encoder
296 return None
297-298 def has_processor(self, type_name: str) -> bool:
299 """Check if a processor is registered for a specific type.
300-301 Args:
302 type_name: The name of the type to check.
303-304 Returns:
305 True if a processor is registered for the specified type, False otherwise.
306 """
307 return type_name in self._processors and bool(self._processors[type_name])
308-309 def clear(self) -> None:
310 """Clear all registered processors."""
311 self._processors.clear()
312-313 def get_registered_types(self) -> set:
314 """Get the set of all registered type names.
315-316 Returns:
317 A set of all registered type names.
318 """
319 return set(self._processors.keys())
320-321 def to_hook_registry(self) -> TypeHookRegistry:
322 """Convert this processor registry to a hook registry.
323-324 This method creates a TypeHookRegistry that uses the decoders from
325 this processor registry.
326-327 Returns:
328 A TypeHookRegistry with the same decoders as this processor registry.
329 """
330 hook_registry = TypeHookRegistry()
331-332 for type_name, processors in self._processors.items():
333 if processors and processors[0].decoder is not None:
334 hook_registry.register_handler(type_name, processors[0].decoder)
335-336 return hook_registry
337338···341342343def register_type(
344- type_name: str,
345- priority: int = 0
346) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
347 """Register a global type decoder function.
348-349 This decorator registers a function as a global decoder for a specific type
350 in the ATProto data model.
351-352 Args:
353 type_name: The name of the type to handle.
354 priority: The priority of this processor (higher values = higher priority).
355-356 Returns:
357 A decorator function that registers the decorated function as a decoder.
358-359 Example:
360 >>> @register_type("app.bsky.feed.post", priority=10)
361 ... def decode_post(data: Dict[str, Any]) -> Any:
···366367def get_global_processor_registry() -> TypeProcessorRegistry:
368 """Get the global type processor registry.
369-370 Returns:
371 The global TypeProcessorRegistry instance.
372 """
···374375376def register_type_encoder(
377- type_name: str,
378- priority: int = 0
379) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
380 """Register a global type encoder function.
381-382 This decorator registers a function as a global encoder for a specific type
383 in the ATProto data model.
384-385 Args:
386 type_name: The name of the type to handle.
387 priority: The priority of this processor (higher values = higher priority).
388-389 Returns:
390 A decorator function that registers the decorated function as an encoder.
391-392 Example:
393 >>> @register_type_encoder("app.bsky.feed.post", priority=10)
394 ... def encode_post(post: Post) -> Dict[str, Any]:
···398399400def register_type_class(
401- type_name: str,
402- priority: int = 0
403) -> Callable[[Type[T]], Type[T]]:
404 """Register a class for both global encoding and decoding.
405-406 This decorator registers a class for both encoding and decoding of a specific type
407 in the ATProto data model.
408-409 Args:
410 type_name: The name of the type to handle.
411 priority: The priority of this processor (higher values = higher priority).
412-413 Returns:
414 A decorator function that registers the decorated class.
415-416 Example:
417 >>> @register_type_class("app.bsky.feed.post", priority=10)
418 ... class Post:
419 ... def __init__(self, text: str, created_at: str) -> None:
420 ... self.text = text
421 ... self.created_at = created_at
422- ...
423 ... @classmethod
424 ... def from_json(cls, data: Dict[str, Any]) -> "Post":
425 ... return cls(data["text"], data["createdAt"])
426- ...
427 ... def to_json(self) -> Dict[str, Any]:
428 ... return {"text": self.text, "createdAt": self.created_at}
429 """
···432433def unregister_type(type_name: str, priority: Optional[int] = None) -> None:
434 """Unregister global type processors.
435-436 Args:
437 type_name: The name of the type to unregister.
438 priority: If specified, only unregister processors with this priority.
···442443def get_type_decoder(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
444 """Get the global decoder function for a specific type.
445-446 Args:
447 type_name: The name of the type to get the decoder for.
448-449 Returns:
450 The decoder function for the specified type, or None if no decoder
451 is registered.
···455456def get_type_encoder(type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
457 """Get the global encoder function for a specific type.
458-459 Args:
460 type_name: The name of the type to get the encoder for.
461-462 Returns:
463 The encoder function for the specified type, or None if no encoder
464 is registered.
···468469def has_type_processor(type_name: str) -> bool:
470 """Check if a global processor is registered for a specific type.
471-472 Args:
473 type_name: The name of the type to check.
474-475 Returns:
476 True if a processor is registered for the specified type, False otherwise.
477 """
···485486def get_registered_types() -> set:
487 """Get the set of all globally registered type names.
488-489 Returns:
490 A set of all registered type names.
491 """
···494495def create_processor_registry() -> TypeProcessorRegistry:
496 """Create a new type processor registry.
497-498 This function creates a new, independent registry that can be used
499 instead of the global registry.
500-501 Returns:
502 A new TypeProcessorRegistry instance.
503 """
504- return TypeProcessorRegistry()
···10from .hooks import TypeHookRegistry
1112# Type variable for the decorated class
13+T = TypeVar("T")
141516class TypeProcessor:
17 """A type processor for ATProto JSON objects.
18+19 This class represents a processor for a specific type in the ATProto data model.
20 It contains information about how to convert JSON data to Python objects and
21 vice versa.
22+23 Attributes:
24 type_name: The name of the type this processor handles.
25 decoder: The function to decode JSON data to a Python object.
26 encoder: The function to encode a Python object to JSON data.
27 priority: The priority of this processor (higher values = higher priority).
28 """
29+30 def __init__(
31 self,
32 type_name: str,
33 decoder: Optional[Callable[[Dict[str, Any]], Any]] = None,
34 encoder: Optional[Callable[[Any], Dict[str, Any]]] = None,
35+ priority: int = 0,
36 ) -> None:
37 """Initialize a type processor.
38+39 Args:
40 type_name: The name of the type this processor handles.
41 decoder: The function to decode JSON data to a Python object.
···46 self.decoder = decoder
47 self.encoder = encoder
48 self.priority = priority
49+50 def decode(self, data: Dict[str, Any]) -> Any:
51 """Decode JSON data to a Python object.
52+53 Args:
54 data: The JSON data to decode.
55+56 Returns:
57 The decoded Python object.
58+59 Raises:
60 ValueError: If no decoder is registered.
61 """
62 if self.decoder is None:
63 raise ValueError(f"No decoder registered for type {self.type_name}")
64 return self.decoder(data)
65+66 def encode(self, obj: Any) -> Dict[str, Any]:
67 """Encode a Python object to JSON data.
68+69 Args:
70 obj: The Python object to encode.
71+72 Returns:
73 The encoded JSON data.
74+75 Raises:
76 ValueError: If no encoder is registered.
77 """
···8283class TypeProcessorRegistry:
84 """Registry for type processors in the ATProto JSON decoder.
85+86 This class maintains a registry of type processors that can be used
87 to customize the encoding and decoding of objects with $type keys in
88 the ATProto data model.
89+90 Attributes:
91 _processors: Dictionary mapping type names to processor lists.
92 """
93+94 def __init__(self) -> None:
95 """Initialize the type processor registry."""
96 self._processors: Dict[str, List[TypeProcessor]] = {}
97+98 def register_processor(self, processor: TypeProcessor) -> None:
99 """Register a type processor.
100+101 Args:
102 processor: The type processor to register.
103 """
104 if processor.type_name not in self._processors:
105 self._processors[processor.type_name] = []
106+107 self._processors[processor.type_name].append(processor)
108 # Sort processors by priority (descending)
109+ self._processors[processor.type_name].sort(
110+ key=lambda p: p.priority, reverse=True
111+ )
112+113 def register(
114+ self, type_name: str, priority: int = 0
00115 ) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
116 """Register a type decoder function.
117+118 This method can be used as a decorator to register a function as a decoder
119 for a specific type.
120+121 Args:
122 type_name: The name of the type to handle.
123 priority: The priority of this processor (higher values = higher priority).
124+125 Returns:
126 A decorator function that registers the decorated function as a decoder.
127+128 Example:
129 >>> registry = TypeProcessorRegistry()
130+ >>>
131 >>> @registry.register("app.bsky.feed.post", priority=10)
132 ... def decode_post(data: Dict[str, Any]) -> Any:
133 ... return Post(**data)
134 """
135+136+ def decorator(
137+ func: Callable[[Dict[str, Any]], Any],
138+ ) -> Callable[[Dict[str, Any]], Any]:
139 processor = TypeProcessor(type_name, decoder=func, priority=priority)
140 self.register_processor(processor)
141 return func
142+143 return decorator
144+145 def register_encoder(
146+ self, type_name: str, priority: int = 0
00147 ) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
148 """Register a type encoder function.
149+150 This method can be used as a decorator to register a function as an encoder
151 for a specific type.
152+153 Args:
154 type_name: The name of the type to handle.
155 priority: The priority of this processor (higher values = higher priority).
156+157 Returns:
158 A decorator function that registers the decorated function as an encoder.
159+160 Example:
161 >>> registry = TypeProcessorRegistry()
162+ >>>
163 >>> @registry.register_encoder("app.bsky.feed.post", priority=10)
164 ... def encode_post(post: Post) -> Dict[str, Any]:
165 ... return {"text": post.text, "createdAt": post.created_at}
166 """
167+168+ def decorator(
169+ func: Callable[[Any], Dict[str, Any]],
170+ ) -> Callable[[Any], Dict[str, Any]]:
171 # Check if a processor for this type already exists
172 if type_name in self._processors:
173 for processor in self._processors[type_name]:
···177 break
178 else:
179 # No decoder found, create a new processor
180+ processor = TypeProcessor(
181+ type_name, encoder=func, priority=priority
182+ )
183 self.register_processor(processor)
184 else:
185 # No processor exists, create a new one
186 processor = TypeProcessor(type_name, encoder=func, priority=priority)
187 self.register_processor(processor)
188+189 return func
190+191 return decorator
192+193 def register_class(
194+ self, type_name: str, priority: int = 0
00195 ) -> Callable[[Type[T]], Type[T]]:
196 """Register a class for both encoding and decoding.
197+198 This method can be used as a decorator to register a class for both
199 encoding and decoding of a specific type.
200+201 The class must have a class method `from_json` that takes a dictionary
202 and returns an instance of the class, and an instance method `to_json`
203 that returns a dictionary.
204+205 Args:
206 type_name: The name of the type to handle.
207 priority: The priority of this processor (higher values = higher priority).
208+209 Returns:
210 A decorator function that registers the decorated class.
211+212 Example:
213 >>> registry = TypeProcessorRegistry()
214+ >>>
215 >>> @registry.register_class("app.bsky.feed.post", priority=10)
216 ... class Post:
217 ... def __init__(self, text: str, created_at: str) -> None:
218 ... self.text = text
219 ... self.created_at = created_at
220+ ...
221 ... @classmethod
222 ... def from_json(cls, data: Dict[str, Any]) -> "Post":
223 ... return cls(data["text"], data["createdAt"])
224+ ...
225 ... def to_json(self) -> Dict[str, Any]:
226 ... return {"text": self.text, "createdAt": self.created_at}
227 """
228+229 def decorator(cls: Type[T]) -> Type[T]:
230 # Create decoder from class method
231 if hasattr(cls, "from_json"):
···237 # Create a decoder that passes the data as keyword arguments
238 decoder = lambda data: cls(**data)
239 else:
240+ raise ValueError(
241+ f"Class {cls.__name__} has no from_json method or compatible __init__"
242+ )
243+244 # Create encoder from instance method
245 if hasattr(cls, "to_json"):
246 encoder = lambda obj: obj.to_json()
247 else:
248 raise ValueError(f"Class {cls.__name__} has no to_json method")
249+250 # Register the processor
251+ processor = TypeProcessor(
252+ type_name, decoder=decoder, encoder=encoder, priority=priority
253+ )
254 self.register_processor(processor)
255+256 return cls
257+258 return decorator
259+260 def unregister(self, type_name: str, priority: Optional[int] = None) -> None:
261 """Unregister type processors.
262+263 Args:
264 type_name: The name of the type to unregister.
265 priority: If specified, only unregister processors with this priority.
···273 else:
274 # Remove all processors for this type
275 del self._processors[type_name]
276+277 def get_decoder(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
278 """Get the decoder function for a specific type.
279+280 Args:
281 type_name: The name of the type to get the decoder for.
282+283 Returns:
284 The decoder function for the specified type, or None if no decoder
285 is registered.
···288 # Return the decoder of the highest priority processor
289 return self._processors[type_name][0].decoder
290 return None
291+292 def get_encoder(self, type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
293 """Get the encoder function for a specific type.
294+295 Args:
296 type_name: The name of the type to get the encoder for.
297+298 Returns:
299 The encoder function for the specified type, or None if no encoder
300 is registered.
···303 # Return the encoder of the highest priority processor
304 return self._processors[type_name][0].encoder
305 return None
306+307 def has_processor(self, type_name: str) -> bool:
308 """Check if a processor is registered for a specific type.
309+310 Args:
311 type_name: The name of the type to check.
312+313 Returns:
314 True if a processor is registered for the specified type, False otherwise.
315 """
316 return type_name in self._processors and bool(self._processors[type_name])
317+318 def clear(self) -> None:
319 """Clear all registered processors."""
320 self._processors.clear()
321+322 def get_registered_types(self) -> set:
323 """Get the set of all registered type names.
324+325 Returns:
326 A set of all registered type names.
327 """
328 return set(self._processors.keys())
329+330 def to_hook_registry(self) -> TypeHookRegistry:
331 """Convert this processor registry to a hook registry.
332+333 This method creates a TypeHookRegistry that uses the decoders from
334 this processor registry.
335+336 Returns:
337 A TypeHookRegistry with the same decoders as this processor registry.
338 """
339 hook_registry = TypeHookRegistry()
340+341 for type_name, processors in self._processors.items():
342 if processors and processors[0].decoder is not None:
343 hook_registry.register_handler(type_name, processors[0].decoder)
344+345 return hook_registry
346347···350351352def register_type(
353+ type_name: str, priority: int = 0
0354) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
355 """Register a global type decoder function.
356+357 This decorator registers a function as a global decoder for a specific type
358 in the ATProto data model.
359+360 Args:
361 type_name: The name of the type to handle.
362 priority: The priority of this processor (higher values = higher priority).
363+364 Returns:
365 A decorator function that registers the decorated function as a decoder.
366+367 Example:
368 >>> @register_type("app.bsky.feed.post", priority=10)
369 ... def decode_post(data: Dict[str, Any]) -> Any:
···374375def get_global_processor_registry() -> TypeProcessorRegistry:
376 """Get the global type processor registry.
377+378 Returns:
379 The global TypeProcessorRegistry instance.
380 """
···382383384def register_type_encoder(
385+ type_name: str, priority: int = 0
0386) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
387 """Register a global type encoder function.
388+389 This decorator registers a function as a global encoder for a specific type
390 in the ATProto data model.
391+392 Args:
393 type_name: The name of the type to handle.
394 priority: The priority of this processor (higher values = higher priority).
395+396 Returns:
397 A decorator function that registers the decorated function as an encoder.
398+399 Example:
400 >>> @register_type_encoder("app.bsky.feed.post", priority=10)
401 ... def encode_post(post: Post) -> Dict[str, Any]:
···405406407def register_type_class(
408+ type_name: str, priority: int = 0
0409) -> Callable[[Type[T]], Type[T]]:
410 """Register a class for both global encoding and decoding.
411+412 This decorator registers a class for both encoding and decoding of a specific type
413 in the ATProto data model.
414+415 Args:
416 type_name: The name of the type to handle.
417 priority: The priority of this processor (higher values = higher priority).
418+419 Returns:
420 A decorator function that registers the decorated class.
421+422 Example:
423 >>> @register_type_class("app.bsky.feed.post", priority=10)
424 ... class Post:
425 ... def __init__(self, text: str, created_at: str) -> None:
426 ... self.text = text
427 ... self.created_at = created_at
428+ ...
429 ... @classmethod
430 ... def from_json(cls, data: Dict[str, Any]) -> "Post":
431 ... return cls(data["text"], data["createdAt"])
432+ ...
433 ... def to_json(self) -> Dict[str, Any]:
434 ... return {"text": self.text, "createdAt": self.created_at}
435 """
···438439def unregister_type(type_name: str, priority: Optional[int] = None) -> None:
440 """Unregister global type processors.
441+442 Args:
443 type_name: The name of the type to unregister.
444 priority: If specified, only unregister processors with this priority.
···448449def get_type_decoder(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
450 """Get the global decoder function for a specific type.
451+452 Args:
453 type_name: The name of the type to get the decoder for.
454+455 Returns:
456 The decoder function for the specified type, or None if no decoder
457 is registered.
···461462def get_type_encoder(type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
463 """Get the global encoder function for a specific type.
464+465 Args:
466 type_name: The name of the type to get the encoder for.
467+468 Returns:
469 The encoder function for the specified type, or None if no encoder
470 is registered.
···474475def has_type_processor(type_name: str) -> bool:
476 """Check if a global processor is registered for a specific type.
477+478 Args:
479 type_name: The name of the type to check.
480+481 Returns:
482 True if a processor is registered for the specified type, False otherwise.
483 """
···491492def get_registered_types() -> set:
493 """Get the set of all globally registered type names.
494+495 Returns:
496 A set of all registered type names.
497 """
···500501def create_processor_registry() -> TypeProcessorRegistry:
502 """Create a new type processor registry.
503+504 This function creates a new, independent registry that can be used
505 instead of the global registry.
506+507 Returns:
508 A new TypeProcessorRegistry instance.
509 """
510+ return TypeProcessorRegistry()
+42-35
src/atpasser/data/wrapper.py
···29 sort_keys: bool = False,
30 encoding: str = "utf-8",
31 type_processor_registry: Optional[TypeProcessorRegistry] = None,
32- **kwargs: Any
33) -> None:
34 """Serialize obj as a JSON formatted stream to fp.
35-36 This function is similar to json.dump() but supports ATProto-specific
37 data types, including bytes, CID links, and typed objects.
38-39 Args:
40 obj: The object to serialize.
41 fp: A file-like object with a write() method.
···70 """
71 if cls is None:
72 cls = JsonEncoder
73-74 # Use the global type processor registry if none is provided
75 if type_processor_registry is None:
76 from .types import get_global_processor_registry
077 type_processor_registry = get_global_processor_registry()
78-79 # Create an encoder instance with the specified encoding and type processor registry
80- encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs)
81-0082 # Use the standard json.dump with our custom encoder
83 json.dump(
84 obj,
···92 separators=separators,
93 default=default,
94 sort_keys=sort_keys,
95- **kwargs
96 )
9798···110 sort_keys: bool = False,
111 encoding: str = "utf-8",
112 type_processor_registry: Optional[TypeProcessorRegistry] = None,
113- **kwargs: Any
114) -> str:
115 """Serialize obj to a JSON formatted string.
116-117 This function is similar to json.dumps() but supports ATProto-specific
118 data types, including bytes, CID links, and typed objects.
119-120 Args:
121 obj: The object to serialize.
122 skipkeys: If True, dict keys that are not basic types (str, int, float,
···147 encoding: The encoding to use for string serialization.
148 type_processor_registry: Registry for type-specific processors.
149 **kwargs: Additional keyword arguments to pass to the JSON encoder.
150-151 Returns:
152 A JSON formatted string.
153 """
154 if cls is None:
155 cls = JsonEncoder
156-157 # Create an encoder instance with the specified encoding and type processor registry
158- encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs)
159-00160 # Use the standard json.dumps with our custom encoder
161 return json.dumps(
162 obj,
···169 separators=separators,
170 default=default,
171 sort_keys=sort_keys,
172- **kwargs
173 )
174175···185 type_hook_registry: Optional[TypeHookRegistry] = None,
186 type_processor_registry: Optional[TypeProcessorRegistry] = None,
187 encoding: str = "utf-8",
188- **kwargs: Any
189) -> Any:
190 """Deserialize fp (a .read()-supporting text file or binary file containing
191 a JSON document) to a Python object.
192-193 This function is similar to json.load() but supports ATProto-specific
194 data types, including bytes, CID links, and typed objects.
195-196 Args:
197 fp: A .read()-supporting text file or binary file containing a JSON document.
198 cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
···220 type_processor_registry: Registry for type-specific processors.
221 encoding: The encoding to use for string deserialization.
222 **kwargs: Additional keyword arguments to pass to the JSON decoder.
223-224 Returns:
225 A Python object.
226 """
227 if cls is None:
228 cls = JsonDecoder
229-230 # Use the global type hook registry if none is provided
231 if type_hook_registry is None and type_processor_registry is None:
232 from .hooks import get_global_registry
0233 type_hook_registry = get_global_registry()
234 elif type_processor_registry is not None:
235 # Convert the type processor registry to a hook registry
236 type_hook_registry = type_processor_registry.to_hook_registry()
237-238 # Create a decoder instance with the specified parameters
239 decoder = cls(
240 object_hook=object_hook,
241 type_hook_registry=type_hook_registry,
242 encoding=encoding,
243- **kwargs
244 )
245-246 # Use the standard json.load with our custom decoder
247 return json.load(
248 fp,
···252 parse_int=parse_int,
253 parse_constant=parse_constant,
254 object_pairs_hook=object_pairs_hook,
255- **kwargs
256 )
257258···268 type_hook_registry: Optional[TypeHookRegistry] = None,
269 type_processor_registry: Optional[TypeProcessorRegistry] = None,
270 encoding: str = "utf-8",
271- **kwargs: Any
272) -> Any:
273 """Deserialize s (a str, bytes or bytearray instance containing a JSON document)
274 to a Python object.
275-276 This function is similar to json.loads() but supports ATProto-specific
277 data types, including bytes, CID links, and typed objects.
278-279 Args:
280 s: A str, bytes or bytearray instance containing a JSON document.
281 cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
···303 type_processor_registry: Registry for type-specific processors.
304 encoding: The encoding to use for string deserialization.
305 **kwargs: Additional keyword arguments to pass to the JSON decoder.
306-307 Returns:
308 A Python object.
309 """
310 if cls is None:
311 cls = JsonDecoder
312-313 # Use the global type hook registry if none is provided
314 if type_hook_registry is None and type_processor_registry is None:
315 from .hooks import get_global_registry
0316 type_hook_registry = get_global_registry()
317 elif type_processor_registry is not None:
318 # Convert the type processor registry to a hook registry
319 type_hook_registry = type_processor_registry.to_hook_registry()
320-321 # Create a decoder instance with the specified parameters
322 decoder = cls(
323 object_hook=object_hook,
324 type_hook_registry=type_hook_registry,
325 encoding=encoding,
326- **kwargs
327 )
328-329 # Use the standard json.loads with our custom decoder
330 return json.loads(
331 s,
···335 parse_int=parse_int,
336 parse_constant=parse_constant,
337 object_pairs_hook=object_pairs_hook,
338- **kwargs
339- )
···29 sort_keys: bool = False,
30 encoding: str = "utf-8",
31 type_processor_registry: Optional[TypeProcessorRegistry] = None,
32+ **kwargs: Any,
33) -> None:
34 """Serialize obj as a JSON formatted stream to fp.
35+36 This function is similar to json.dump() but supports ATProto-specific
37 data types, including bytes, CID links, and typed objects.
38+39 Args:
40 obj: The object to serialize.
41 fp: A file-like object with a write() method.
···70 """
71 if cls is None:
72 cls = JsonEncoder
73+74 # Use the global type processor registry if none is provided
75 if type_processor_registry is None:
76 from .types import get_global_processor_registry
77+78 type_processor_registry = get_global_processor_registry()
79+80 # Create an encoder instance with the specified encoding and type processor registry
81+ encoder = cls(
82+ encoding=encoding, type_processor_registry=type_processor_registry, **kwargs
83+ )
84+85 # Use the standard json.dump with our custom encoder
86 json.dump(
87 obj,
···95 separators=separators,
96 default=default,
97 sort_keys=sort_keys,
98+ **kwargs,
99 )
100101···113 sort_keys: bool = False,
114 encoding: str = "utf-8",
115 type_processor_registry: Optional[TypeProcessorRegistry] = None,
116+ **kwargs: Any,
117) -> str:
118 """Serialize obj to a JSON formatted string.
119+120 This function is similar to json.dumps() but supports ATProto-specific
121 data types, including bytes, CID links, and typed objects.
122+123 Args:
124 obj: The object to serialize.
125 skipkeys: If True, dict keys that are not basic types (str, int, float,
···150 encoding: The encoding to use for string serialization.
151 type_processor_registry: Registry for type-specific processors.
152 **kwargs: Additional keyword arguments to pass to the JSON encoder.
153+154 Returns:
155 A JSON formatted string.
156 """
157 if cls is None:
158 cls = JsonEncoder
159+160 # Create an encoder instance with the specified encoding and type processor registry
161+ encoder = cls(
162+ encoding=encoding, type_processor_registry=type_processor_registry, **kwargs
163+ )
164+165 # Use the standard json.dumps with our custom encoder
166 return json.dumps(
167 obj,
···174 separators=separators,
175 default=default,
176 sort_keys=sort_keys,
177+ **kwargs,
178 )
179180···190 type_hook_registry: Optional[TypeHookRegistry] = None,
191 type_processor_registry: Optional[TypeProcessorRegistry] = None,
192 encoding: str = "utf-8",
193+ **kwargs: Any,
194) -> Any:
195 """Deserialize fp (a .read()-supporting text file or binary file containing
196 a JSON document) to a Python object.
197+198 This function is similar to json.load() but supports ATProto-specific
199 data types, including bytes, CID links, and typed objects.
200+201 Args:
202 fp: A .read()-supporting text file or binary file containing a JSON document.
203 cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
···225 type_processor_registry: Registry for type-specific processors.
226 encoding: The encoding to use for string deserialization.
227 **kwargs: Additional keyword arguments to pass to the JSON decoder.
228+229 Returns:
230 A Python object.
231 """
232 if cls is None:
233 cls = JsonDecoder
234+235 # Use the global type hook registry if none is provided
236 if type_hook_registry is None and type_processor_registry is None:
237 from .hooks import get_global_registry
238+239 type_hook_registry = get_global_registry()
240 elif type_processor_registry is not None:
241 # Convert the type processor registry to a hook registry
242 type_hook_registry = type_processor_registry.to_hook_registry()
243+244 # Create a decoder instance with the specified parameters
245 decoder = cls(
246 object_hook=object_hook,
247 type_hook_registry=type_hook_registry,
248 encoding=encoding,
249+ **kwargs,
250 )
251+252 # Use the standard json.load with our custom decoder
253 return json.load(
254 fp,
···258 parse_int=parse_int,
259 parse_constant=parse_constant,
260 object_pairs_hook=object_pairs_hook,
261+ **kwargs,
262 )
263264···274 type_hook_registry: Optional[TypeHookRegistry] = None,
275 type_processor_registry: Optional[TypeProcessorRegistry] = None,
276 encoding: str = "utf-8",
277+ **kwargs: Any,
278) -> Any:
279 """Deserialize s (a str, bytes or bytearray instance containing a JSON document)
280 to a Python object.
281+282 This function is similar to json.loads() but supports ATProto-specific
283 data types, including bytes, CID links, and typed objects.
284+285 Args:
286 s: A str, bytes or bytearray instance containing a JSON document.
287 cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
···309 type_processor_registry: Registry for type-specific processors.
310 encoding: The encoding to use for string deserialization.
311 **kwargs: Additional keyword arguments to pass to the JSON decoder.
312+313 Returns:
314 A Python object.
315 """
316 if cls is None:
317 cls = JsonDecoder
318+319 # Use the global type hook registry if none is provided
320 if type_hook_registry is None and type_processor_registry is None:
321 from .hooks import get_global_registry
322+323 type_hook_registry = get_global_registry()
324 elif type_processor_registry is not None:
325 # Convert the type processor registry to a hook registry
326 type_hook_registry = type_processor_registry.to_hook_registry()
327+328 # Create a decoder instance with the specified parameters
329 decoder = cls(
330 object_hook=object_hook,
331 type_hook_registry=type_hook_registry,
332 encoding=encoding,
333+ **kwargs,
334 )
335+336 # Use the standard json.loads with our custom decoder
337 return json.loads(
338 s,
···342 parse_int=parse_int,
343 parse_constant=parse_constant,
344 object_pairs_hook=object_pairs_hook,
345+ **kwargs,
346+ )
···12 """Test creating a DID with a valid did:plc format."""
13 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
14 did = DID(did_str)
15-16 assert str(did) == did_str
17 assert did.uri == did_str
18···20 """Test creating a DID with a valid did:web format."""
21 did_str = "did:web:blueskyweb.xyz"
22 did = DID(did_str)
23-24 assert str(did) == did_str
25 assert did.uri == did_str
26···28 """Test creating a DID with various valid characters."""
29 did_str = "did:method:val:two-with_underscores.and-dashes"
30 did = DID(did_str)
31-32 assert str(did) == did_str
33 assert did.uri == did_str
3435 def test_invalid_did_wrong_format(self):
36 """Test that a DID with wrong format raises InvalidDIDError."""
37 did_str = "not-a-did"
38-39 with pytest.raises(InvalidDIDError, match="invalid format"):
40 DID(did_str)
4142 def test_invalid_did_uppercase_method(self):
43 """Test that a DID with uppercase method raises InvalidDIDError."""
44 did_str = "did:METHOD:val"
45-46 with pytest.raises(InvalidDIDError, match="invalid format"):
47 DID(did_str)
4849 def test_invalid_did_method_with_numbers(self):
50 """Test that a DID with method containing numbers raises InvalidDIDError."""
51 did_str = "did:m123:val"
52-53 with pytest.raises(InvalidDIDError, match="invalid format"):
54 DID(did_str)
5556 def test_invalid_did_empty_identifier(self):
57 """Test that a DID with empty identifier raises InvalidDIDError."""
58 did_str = "did:method:"
59-60 with pytest.raises(InvalidDIDError, match="invalid format"):
61 DID(did_str)
6263 def test_invalid_did_ends_with_colon(self):
64 """Test that a DID ending with colon raises InvalidDIDError."""
65 did_str = "did:method:val:"
66-67 with pytest.raises(InvalidDIDError, match="invalid format"):
68 DID(did_str)
69···72 # Create a DID that exceeds the 2048 character limit
73 long_identifier = "a" * 2040
74 did_str = f"did:method:{long_identifier}"
75-76 with pytest.raises(InvalidDIDError, match="exceeds maximum length"):
77 DID(did_str)
78···81 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
82 did1 = DID(did_str)
83 did2 = DID(did_str)
84-85 assert did1 == did2
86 assert did1 != "not a did object"
87···89 """Test DID string representation."""
90 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
91 did = DID(did_str)
92-93 assert str(did) == did_str
9495 def test_did_fetch_plc_method(self):
96 """Test fetching a DID document for did:plc method."""
97 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
98 did = DID(did_str)
99-100 # This test may fail if there's no internet connection or if the PLC directory is down
101 try:
102 document = did.fetch()
···110 """Test fetching a DID document for did:web method."""
111 did_str = "did:web:blueskyweb.xyz"
112 did = DID(did_str)
113-114 # This test may fail if there's no internet connection or if the web server is down
115 try:
116 document = did.fetch()
···124 """Test that fetching a DID document with unsupported method raises InvalidDIDError."""
125 did_str = "did:unsupported:method"
126 did = DID(did_str)
127-128 with pytest.raises(InvalidDIDError, match="unsupported DID method"):
129 did.fetch()
130···132 """Test that fetching a DID document with empty domain raises InvalidDIDError."""
133 did_str = "did:web:"
134 did = DID(did_str)
135-136 with pytest.raises(InvalidDIDError, match="invalid format"):
137- did.fetch()
···12 """Test creating a DID with a valid did:plc format."""
13 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
14 did = DID(did_str)
15+16 assert str(did) == did_str
17 assert did.uri == did_str
18···20 """Test creating a DID with a valid did:web format."""
21 did_str = "did:web:blueskyweb.xyz"
22 did = DID(did_str)
23+24 assert str(did) == did_str
25 assert did.uri == did_str
26···28 """Test creating a DID with various valid characters."""
29 did_str = "did:method:val:two-with_underscores.and-dashes"
30 did = DID(did_str)
31+32 assert str(did) == did_str
33 assert did.uri == did_str
3435 def test_invalid_did_wrong_format(self):
36 """Test that a DID with wrong format raises InvalidDIDError."""
37 did_str = "not-a-did"
38+39 with pytest.raises(InvalidDIDError, match="invalid format"):
40 DID(did_str)
4142 def test_invalid_did_uppercase_method(self):
43 """Test that a DID with uppercase method raises InvalidDIDError."""
44 did_str = "did:METHOD:val"
45+46 with pytest.raises(InvalidDIDError, match="invalid format"):
47 DID(did_str)
4849 def test_invalid_did_method_with_numbers(self):
50 """Test that a DID with method containing numbers raises InvalidDIDError."""
51 did_str = "did:m123:val"
52+53 with pytest.raises(InvalidDIDError, match="invalid format"):
54 DID(did_str)
5556 def test_invalid_did_empty_identifier(self):
57 """Test that a DID with empty identifier raises InvalidDIDError."""
58 did_str = "did:method:"
59+60 with pytest.raises(InvalidDIDError, match="invalid format"):
61 DID(did_str)
6263 def test_invalid_did_ends_with_colon(self):
64 """Test that a DID ending with colon raises InvalidDIDError."""
65 did_str = "did:method:val:"
66+67 with pytest.raises(InvalidDIDError, match="invalid format"):
68 DID(did_str)
69···72 # Create a DID that exceeds the 2048 character limit
73 long_identifier = "a" * 2040
74 did_str = f"did:method:{long_identifier}"
75+76 with pytest.raises(InvalidDIDError, match="exceeds maximum length"):
77 DID(did_str)
78···81 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
82 did1 = DID(did_str)
83 did2 = DID(did_str)
84+85 assert did1 == did2
86 assert did1 != "not a did object"
87···89 """Test DID string representation."""
90 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
91 did = DID(did_str)
92+93 assert str(did) == did_str
9495 def test_did_fetch_plc_method(self):
96 """Test fetching a DID document for did:plc method."""
97 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
98 did = DID(did_str)
99+100 # This test may fail if there's no internet connection or if the PLC directory is down
101 try:
102 document = did.fetch()
···110 """Test fetching a DID document for did:web method."""
111 did_str = "did:web:blueskyweb.xyz"
112 did = DID(did_str)
113+114 # This test may fail if there's no internet connection or if the web server is down
115 try:
116 document = did.fetch()
···124 """Test that fetching a DID document with unsupported method raises InvalidDIDError."""
125 did_str = "did:unsupported:method"
126 did = DID(did_str)
127+128 with pytest.raises(InvalidDIDError, match="unsupported DID method"):
129 did.fetch()
130···132 """Test that fetching a DID document with empty domain raises InvalidDIDError."""
133 did_str = "did:web:"
134 did = DID(did_str)
135+136 with pytest.raises(InvalidDIDError, match="invalid format"):
137+ did.fetch()
+22-22
tests/uri/test_handle.py
···12 """Test creating a Handle with a valid simple format."""
13 handle_str = "example.com"
14 handle = Handle(handle_str)
15-16 assert str(handle) == handle_str
17 assert handle.handle == handle_str
18···20 """Test creating a Handle with a valid subdomain format."""
21 handle_str = "subdomain.example.com"
22 handle = Handle(handle_str)
23-24 assert str(handle) == handle_str
25 assert handle.handle == handle_str
26···28 """Test creating a Handle with a valid format containing hyphens."""
29 handle_str = "my-example.com"
30 handle = Handle(handle_str)
31-32 assert str(handle) == handle_str
33 assert handle.handle == handle_str
34···36 """Test creating a Handle with a valid format containing numbers."""
37 handle_str = "example123.com"
38 handle = Handle(handle_str)
39-40 assert str(handle) == handle_str
41 assert handle.handle == handle_str
42···44 """Test creating a Handle with a valid long domain name."""
45 handle_str = "a" * 63 + "." + "b" * 63 + "." + "c" * 63 + ".com"
46 handle = Handle(handle_str)
47-48 assert str(handle) == handle_str
49 assert handle.handle == handle_str
50···53 # Create a handle that exceeds the 253 character limit
54 long_handle = "a" * 254
55 handle_str = f"{long_handle}.com"
56-57 with pytest.raises(InvalidHandleError, match="exceeds maximum length"):
58 Handle(handle_str)
5960 def test_invalid_handle_no_dot_separator(self):
61 """Test that a Handle without a dot separator raises InvalidHandleError."""
62 handle_str = "example"
63-64 with pytest.raises(InvalidHandleError, match="invalid format"):
65 Handle(handle_str)
6667 def test_invalid_handle_starts_with_dot(self):
68 """Test that a Handle starting with a dot raises InvalidHandleError."""
69 handle_str = ".example.com"
70-71 with pytest.raises(InvalidHandleError, match="invalid format"):
72 Handle(handle_str)
7374 def test_invalid_handle_ends_with_dot(self):
75 """Test that a Handle ending with a dot raises InvalidHandleError."""
76 handle_str = "example.com."
77-78 with pytest.raises(InvalidHandleError, match="invalid format"):
79 Handle(handle_str)
8081 def test_invalid_handle_segment_too_long(self):
82 """Test that a Handle with a segment that is too long raises InvalidHandleError."""
83 handle_str = f"{'a' * 64}.com"
84-85 with pytest.raises(InvalidHandleError, match="segment length error"):
86 Handle(handle_str)
8788 def test_invalid_handle_segment_empty(self):
89 """Test that a Handle with an empty segment raises InvalidHandleError."""
90 handle_str = "example..com"
91-92 with pytest.raises(InvalidHandleError, match="segment length error"):
93 Handle(handle_str)
9495 def test_invalid_handle_invalid_characters(self):
96 """Test that a Handle with invalid characters raises InvalidHandleError."""
97 handle_str = "ex@mple.com"
98-99 with pytest.raises(InvalidHandleError, match="contains invalid characters"):
100 Handle(handle_str)
101102 def test_invalid_handle_segment_starts_with_hyphen(self):
103 """Test that a Handle with a segment starting with a hyphen raises InvalidHandleError."""
104 handle_str = "-example.com"
105-106 with pytest.raises(InvalidHandleError, match="invalid format"):
107 Handle(handle_str)
108109 def test_invalid_handle_segment_ends_with_hyphen(self):
110 """Test that a Handle with a segment ending with a hyphen raises InvalidHandleError."""
111 handle_str = "example-.com"
112-113 with pytest.raises(InvalidHandleError, match="invalid format"):
114 Handle(handle_str)
115116 def test_invalid_handle_tld_starts_with_digit(self):
117 """Test that a Handle with a TLD starting with a digit raises InvalidHandleError."""
118 handle_str = "example.1com"
119-120 with pytest.raises(InvalidHandleError, match="invalid format"):
121 Handle(handle_str)
122···125 handle_str = "example.com"
126 handle1 = Handle(handle_str)
127 handle2 = Handle(handle_str)
128-129 assert handle1 == handle2
130 assert handle1 != "not a handle object"
131···133 """Test Handle string representation."""
134 handle_str = "example.com"
135 handle = Handle(handle_str)
136-137 assert str(handle) == handle_str
138139 def test_handle_case_insensitive_storage(self):
140 """Test that Handle stores the handle in lowercase."""
141 handle_str = "ExAmPlE.CoM"
142 handle = Handle(handle_str)
143-144 # The handle should be stored in lowercase
145 assert handle.handle == "example.com"
146 # The string representation should also return the lowercase form
···150 """Test resolving a handle to DID using DNS method."""
151 handle_str = "bsky.app"
152 handle = Handle(handle_str)
153-154 # This test may fail if there's no internet connection or if DNS resolution fails
155 try:
156 did = handle.toTID()
···164 """Test resolving a handle to DID using HTTP method."""
165 handle_str = "blueskyweb.xyz"
166 handle = Handle(handle_str)
167-168 # This test may fail if there's no internet connection or if HTTP resolution fails
169 try:
170 did = handle.toTID()
···178 """Test resolving an unresolvable handle returns None."""
179 handle_str = "nonexistent-domain-12345.com"
180 handle = Handle(handle_str)
181-182 # This should return None for a non-existent domain
183 did = handle.toTID()
184- assert did is None
···12 """Test creating a Handle with a valid simple format."""
13 handle_str = "example.com"
14 handle = Handle(handle_str)
15+16 assert str(handle) == handle_str
17 assert handle.handle == handle_str
18···20 """Test creating a Handle with a valid subdomain format."""
21 handle_str = "subdomain.example.com"
22 handle = Handle(handle_str)
23+24 assert str(handle) == handle_str
25 assert handle.handle == handle_str
26···28 """Test creating a Handle with a valid format containing hyphens."""
29 handle_str = "my-example.com"
30 handle = Handle(handle_str)
31+32 assert str(handle) == handle_str
33 assert handle.handle == handle_str
34···36 """Test creating a Handle with a valid format containing numbers."""
37 handle_str = "example123.com"
38 handle = Handle(handle_str)
39+40 assert str(handle) == handle_str
41 assert handle.handle == handle_str
42···44 """Test creating a Handle with a valid long domain name."""
45 handle_str = "a" * 63 + "." + "b" * 63 + "." + "c" * 63 + ".com"
46 handle = Handle(handle_str)
47+48 assert str(handle) == handle_str
49 assert handle.handle == handle_str
50···53 # Create a handle that exceeds the 253 character limit
54 long_handle = "a" * 254
55 handle_str = f"{long_handle}.com"
56+57 with pytest.raises(InvalidHandleError, match="exceeds maximum length"):
58 Handle(handle_str)
5960 def test_invalid_handle_no_dot_separator(self):
61 """Test that a Handle without a dot separator raises InvalidHandleError."""
62 handle_str = "example"
63+64 with pytest.raises(InvalidHandleError, match="invalid format"):
65 Handle(handle_str)
6667 def test_invalid_handle_starts_with_dot(self):
68 """Test that a Handle starting with a dot raises InvalidHandleError."""
69 handle_str = ".example.com"
70+71 with pytest.raises(InvalidHandleError, match="invalid format"):
72 Handle(handle_str)
7374 def test_invalid_handle_ends_with_dot(self):
75 """Test that a Handle ending with a dot raises InvalidHandleError."""
76 handle_str = "example.com."
77+78 with pytest.raises(InvalidHandleError, match="invalid format"):
79 Handle(handle_str)
8081 def test_invalid_handle_segment_too_long(self):
82 """Test that a Handle with a segment that is too long raises InvalidHandleError."""
83 handle_str = f"{'a' * 64}.com"
84+85 with pytest.raises(InvalidHandleError, match="segment length error"):
86 Handle(handle_str)
8788 def test_invalid_handle_segment_empty(self):
89 """Test that a Handle with an empty segment raises InvalidHandleError."""
90 handle_str = "example..com"
91+92 with pytest.raises(InvalidHandleError, match="segment length error"):
93 Handle(handle_str)
9495 def test_invalid_handle_invalid_characters(self):
96 """Test that a Handle with invalid characters raises InvalidHandleError."""
97 handle_str = "ex@mple.com"
98+99 with pytest.raises(InvalidHandleError, match="contains invalid characters"):
100 Handle(handle_str)
101102 def test_invalid_handle_segment_starts_with_hyphen(self):
103 """Test that a Handle with a segment starting with a hyphen raises InvalidHandleError."""
104 handle_str = "-example.com"
105+106 with pytest.raises(InvalidHandleError, match="invalid format"):
107 Handle(handle_str)
108109 def test_invalid_handle_segment_ends_with_hyphen(self):
110 """Test that a Handle with a segment ending with a hyphen raises InvalidHandleError."""
111 handle_str = "example-.com"
112+113 with pytest.raises(InvalidHandleError, match="invalid format"):
114 Handle(handle_str)
115116 def test_invalid_handle_tld_starts_with_digit(self):
117 """Test that a Handle with a TLD starting with a digit raises InvalidHandleError."""
118 handle_str = "example.1com"
119+120 with pytest.raises(InvalidHandleError, match="invalid format"):
121 Handle(handle_str)
122···125 handle_str = "example.com"
126 handle1 = Handle(handle_str)
127 handle2 = Handle(handle_str)
128+129 assert handle1 == handle2
130 assert handle1 != "not a handle object"
131···133 """Test Handle string representation."""
134 handle_str = "example.com"
135 handle = Handle(handle_str)
136+137 assert str(handle) == handle_str
138139 def test_handle_case_insensitive_storage(self):
140 """Test that Handle stores the handle in lowercase."""
141 handle_str = "ExAmPlE.CoM"
142 handle = Handle(handle_str)
143+144 # The handle should be stored in lowercase
145 assert handle.handle == "example.com"
146 # The string representation should also return the lowercase form
···150 """Test resolving a handle to DID using DNS method."""
151 handle_str = "bsky.app"
152 handle = Handle(handle_str)
153+154 # This test may fail if there's no internet connection or if DNS resolution fails
155 try:
156 did = handle.toTID()
···164 """Test resolving a handle to DID using HTTP method."""
165 handle_str = "blueskyweb.xyz"
166 handle = Handle(handle_str)
167+168 # This test may fail if there's no internet connection or if HTTP resolution fails
169 try:
170 did = handle.toTID()
···178 """Test resolving an unresolvable handle returns None."""
179 handle_str = "nonexistent-domain-12345.com"
180 handle = Handle(handle_str)
181+182 # This should return None for a non-existent domain
183 did = handle.toTID()
184+ assert did is None
+39-33
tests/uri/test_nsid.py
···12 """Test creating an NSID with a valid simple format."""
13 nsid_str = "com.example.recordName"
14 nsid = NSID(nsid_str)
15-16 assert str(nsid) == nsid_str
17 assert nsid.nsid == nsid_str
18 assert nsid.domainAuthority == ["com", "example"]
···24 """Test creating an NSID with a valid fragment."""
25 nsid_str = "com.example.recordName#fragment"
26 nsid = NSID(nsid_str)
27-28 assert str(nsid) == nsid_str
29 assert nsid.nsid == nsid_str
30 assert nsid.domainAuthority == ["com", "example"]
···36 """Test creating an NSID with multiple domain segments."""
37 nsid_str = "net.users.bob.ping"
38 nsid = NSID(nsid_str)
39-40 assert str(nsid) == nsid_str
41 assert nsid.nsid == nsid_str
42 assert nsid.domainAuthority == ["net", "users", "bob"]
···48 """Test creating an NSID with hyphens in domain segments."""
49 nsid_str = "a-0.b-1.c.recordName"
50 nsid = NSID(nsid_str)
51-52 assert str(nsid) == nsid_str
53 assert nsid.nsid == nsid_str
54 assert nsid.domainAuthority == ["a-0", "b-1", "c"]
···60 """Test creating an NSID with case-sensitive name."""
61 nsid_str = "com.example.fooBar"
62 nsid = NSID(nsid_str)
63-64 assert str(nsid) == nsid_str
65 assert nsid.nsid == nsid_str
66 assert nsid.domainAuthority == ["com", "example"]
···72 """Test creating an NSID with numbers in the name."""
73 nsid_str = "com.example.record123"
74 nsid = NSID(nsid_str)
75-76 assert str(nsid) == nsid_str
77 assert nsid.nsid == nsid_str
78 assert nsid.domainAuthority == ["com", "example"]
···83 def test_invalid_nsid_non_ascii_characters(self):
84 """Test that an NSID with non-ASCII characters raises InvalidNSIDError."""
85 nsid_str = "com.exa💩ple.thing"
86-87 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
88 NSID(nsid_str)
89···92 # Create an NSID that exceeds the 317 character limit
93 long_segment = "a" * 100
94 nsid_str = f"{long_segment}.{long_segment}.{long_segment}.recordName"
95-96- with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"):
0097 NSID(nsid_str)
9899 def test_invalid_nsid_starts_with_dot(self):
100 """Test that an NSID starting with a dot raises InvalidNSIDError."""
101 nsid_str = ".com.example.recordName"
102-103 with pytest.raises(InvalidNSIDError, match="invalid format"):
104 NSID(nsid_str)
105106 def test_invalid_nsid_ends_with_dot(self):
107 """Test that an NSID ending with a dot raises InvalidNSIDError."""
108 nsid_str = "com.example.recordName."
109-110 with pytest.raises(InvalidNSIDError, match="invalid format"):
111 NSID(nsid_str)
112113 def test_invalid_nsid_too_few_segments(self):
114 """Test that an NSID with too few segments raises InvalidNSIDError."""
115 nsid_str = "com.example"
116-117 with pytest.raises(InvalidNSIDError, match="invalid format"):
118 NSID(nsid_str)
119···121 """Test that an NSID with domain authority that is too long raises InvalidNSIDError."""
122 # Create a domain authority that exceeds the 253 character limit
123 long_segment = "a" * 63
124- nsid_str = f"{long_segment}.{long_segment}.{long_segment}.{long_segment}.recordName"
125-126- with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"):
0000127 NSID(nsid_str)
128129 def test_invalid_nsid_domain_segment_too_long(self):
130 """Test that an NSID with a domain segment that is too long raises InvalidNSIDError."""
131 nsid_str = f"{'a' * 64}.example.recordName"
132-133 with pytest.raises(InvalidNSIDError, match="segment length error"):
134 NSID(nsid_str)
135136 def test_invalid_nsid_domain_segment_empty(self):
137 """Test that an NSID with an empty domain segment raises InvalidNSIDError."""
138 nsid_str = "com..example.recordName"
139-140 with pytest.raises(InvalidNSIDError, match="segment length error"):
141 NSID(nsid_str)
142143 def test_invalid_nsid_domain_invalid_characters(self):
144 """Test that an NSID with invalid characters in domain raises InvalidNSIDError."""
145 nsid_str = "com.ex@mple.recordName"
146-147 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
148 NSID(nsid_str)
149150 def test_invalid_nsid_domain_segment_starts_with_hyphen(self):
151 """Test that an NSID with a domain segment starting with a hyphen raises InvalidNSIDError."""
152 nsid_str = "com.-example.recordName"
153-154 with pytest.raises(InvalidNSIDError, match="invalid format"):
155 NSID(nsid_str)
156157 def test_invalid_nsid_domain_segment_ends_with_hyphen(self):
158 """Test that an NSID with a domain segment ending with a hyphen raises InvalidNSIDError."""
159 nsid_str = "com.example-.recordName"
160-161 with pytest.raises(InvalidNSIDError, match="invalid format"):
162 NSID(nsid_str)
163164 def test_invalid_nsid_tld_starts_with_digit(self):
165 """Test that an NSID with a TLD starting with a digit raises InvalidNSIDError."""
166 nsid_str = "1com.example.recordName"
167-168 with pytest.raises(InvalidNSIDError, match="invalid format"):
169 NSID(nsid_str)
170171 def test_invalid_nsid_name_empty(self):
172 """Test that an NSID with an empty name raises InvalidNSIDError."""
173 nsid_str = "com.example."
174-175 with pytest.raises(InvalidNSIDError, match="invalid format"):
176 NSID(nsid_str)
177178 def test_invalid_nsid_name_too_long(self):
179 """Test that an NSID with a name that is too long raises InvalidNSIDError."""
180 nsid_str = f"com.example.{'a' * 64}"
181-182 with pytest.raises(InvalidNSIDError, match="name length error"):
183 NSID(nsid_str)
184185 def test_invalid_nsid_name_invalid_characters(self):
186 """Test that an NSID with invalid characters in name raises InvalidNSIDError."""
187 nsid_str = "com.example.record-name"
188-189 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
190 NSID(nsid_str)
191192 def test_invalid_nsid_name_starts_with_digit(self):
193 """Test that an NSID with a name starting with a digit raises InvalidNSIDError."""
194 nsid_str = "com.example.1record"
195-196 with pytest.raises(InvalidNSIDError, match="invalid format"):
197 NSID(nsid_str)
198199 def test_invalid_nsid_fragment_empty(self):
200 """Test that an NSID with an empty fragment raises InvalidNSIDError."""
201 nsid_str = "com.example.recordName#"
202-203 with pytest.raises(InvalidNSIDError, match="fragment length error"):
204 NSID(nsid_str)
205206 def test_invalid_nsid_fragment_too_long(self):
207 """Test that an NSID with a fragment that is too long raises InvalidNSIDError."""
208 nsid_str = f"com.example.recordName#{'a' * 64}"
209-210 with pytest.raises(InvalidNSIDError, match="fragment length error"):
211 NSID(nsid_str)
212213 def test_invalid_nsid_fragment_invalid_characters(self):
214 """Test that an NSID with invalid characters in fragment raises InvalidNSIDError."""
215 nsid_str = "com.example.recordName#fragment-with-hyphen"
216-217 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
218 NSID(nsid_str)
219220 def test_invalid_nsid_fragment_starts_with_digit(self):
221 """Test that an NSID with a fragment starting with a digit raises InvalidNSIDError."""
222 nsid_str = "com.example.recordName#1fragment"
223-224 with pytest.raises(InvalidNSIDError, match="invalid format"):
225 NSID(nsid_str)
226···229 nsid_str = "com.example.recordName"
230 nsid1 = NSID(nsid_str)
231 nsid2 = NSID(nsid_str)
232-233 assert nsid1 == nsid2
234 assert nsid1 != "not an nsid object"
235···237 """Test NSID string representation."""
238 nsid_str = "com.example.recordName"
239 nsid = NSID(nsid_str)
240-241 assert str(nsid) == nsid_str
242243 def test_nsid_string_representation_with_fragment(self):
244 """Test NSID string representation with fragment."""
245 nsid_str = "com.example.recordName#fragment"
246 nsid = NSID(nsid_str)
247-248- assert str(nsid) == nsid_str
···12 """Test creating an NSID with a valid simple format."""
13 nsid_str = "com.example.recordName"
14 nsid = NSID(nsid_str)
15+16 assert str(nsid) == nsid_str
17 assert nsid.nsid == nsid_str
18 assert nsid.domainAuthority == ["com", "example"]
···24 """Test creating an NSID with a valid fragment."""
25 nsid_str = "com.example.recordName#fragment"
26 nsid = NSID(nsid_str)
27+28 assert str(nsid) == nsid_str
29 assert nsid.nsid == nsid_str
30 assert nsid.domainAuthority == ["com", "example"]
···36 """Test creating an NSID with multiple domain segments."""
37 nsid_str = "net.users.bob.ping"
38 nsid = NSID(nsid_str)
39+40 assert str(nsid) == nsid_str
41 assert nsid.nsid == nsid_str
42 assert nsid.domainAuthority == ["net", "users", "bob"]
···48 """Test creating an NSID with hyphens in domain segments."""
49 nsid_str = "a-0.b-1.c.recordName"
50 nsid = NSID(nsid_str)
51+52 assert str(nsid) == nsid_str
53 assert nsid.nsid == nsid_str
54 assert nsid.domainAuthority == ["a-0", "b-1", "c"]
···60 """Test creating an NSID with case-sensitive name."""
61 nsid_str = "com.example.fooBar"
62 nsid = NSID(nsid_str)
63+64 assert str(nsid) == nsid_str
65 assert nsid.nsid == nsid_str
66 assert nsid.domainAuthority == ["com", "example"]
···72 """Test creating an NSID with numbers in the name."""
73 nsid_str = "com.example.record123"
74 nsid = NSID(nsid_str)
75+76 assert str(nsid) == nsid_str
77 assert nsid.nsid == nsid_str
78 assert nsid.domainAuthority == ["com", "example"]
···83 def test_invalid_nsid_non_ascii_characters(self):
84 """Test that an NSID with non-ASCII characters raises InvalidNSIDError."""
85 nsid_str = "com.exa💩ple.thing"
86+87 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
88 NSID(nsid_str)
89···92 # Create an NSID that exceeds the 317 character limit
93 long_segment = "a" * 100
94 nsid_str = f"{long_segment}.{long_segment}.{long_segment}.recordName"
95+96+ with pytest.raises(
97+ InvalidNSIDError, match="domain authority length exceeds limit"
98+ ):
99 NSID(nsid_str)
100101 def test_invalid_nsid_starts_with_dot(self):
102 """Test that an NSID starting with a dot raises InvalidNSIDError."""
103 nsid_str = ".com.example.recordName"
104+105 with pytest.raises(InvalidNSIDError, match="invalid format"):
106 NSID(nsid_str)
107108 def test_invalid_nsid_ends_with_dot(self):
109 """Test that an NSID ending with a dot raises InvalidNSIDError."""
110 nsid_str = "com.example.recordName."
111+112 with pytest.raises(InvalidNSIDError, match="invalid format"):
113 NSID(nsid_str)
114115 def test_invalid_nsid_too_few_segments(self):
116 """Test that an NSID with too few segments raises InvalidNSIDError."""
117 nsid_str = "com.example"
118+119 with pytest.raises(InvalidNSIDError, match="invalid format"):
120 NSID(nsid_str)
121···123 """Test that an NSID with domain authority that is too long raises InvalidNSIDError."""
124 # Create a domain authority that exceeds the 253 character limit
125 long_segment = "a" * 63
126+ nsid_str = (
127+ f"{long_segment}.{long_segment}.{long_segment}.{long_segment}.recordName"
128+ )
129+130+ with pytest.raises(
131+ InvalidNSIDError, match="domain authority length exceeds limit"
132+ ):
133 NSID(nsid_str)
134135 def test_invalid_nsid_domain_segment_too_long(self):
136 """Test that an NSID with a domain segment that is too long raises InvalidNSIDError."""
137 nsid_str = f"{'a' * 64}.example.recordName"
138+139 with pytest.raises(InvalidNSIDError, match="segment length error"):
140 NSID(nsid_str)
141142 def test_invalid_nsid_domain_segment_empty(self):
143 """Test that an NSID with an empty domain segment raises InvalidNSIDError."""
144 nsid_str = "com..example.recordName"
145+146 with pytest.raises(InvalidNSIDError, match="segment length error"):
147 NSID(nsid_str)
148149 def test_invalid_nsid_domain_invalid_characters(self):
150 """Test that an NSID with invalid characters in domain raises InvalidNSIDError."""
151 nsid_str = "com.ex@mple.recordName"
152+153 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
154 NSID(nsid_str)
155156 def test_invalid_nsid_domain_segment_starts_with_hyphen(self):
157 """Test that an NSID with a domain segment starting with a hyphen raises InvalidNSIDError."""
158 nsid_str = "com.-example.recordName"
159+160 with pytest.raises(InvalidNSIDError, match="invalid format"):
161 NSID(nsid_str)
162163 def test_invalid_nsid_domain_segment_ends_with_hyphen(self):
164 """Test that an NSID with a domain segment ending with a hyphen raises InvalidNSIDError."""
165 nsid_str = "com.example-.recordName"
166+167 with pytest.raises(InvalidNSIDError, match="invalid format"):
168 NSID(nsid_str)
169170 def test_invalid_nsid_tld_starts_with_digit(self):
171 """Test that an NSID with a TLD starting with a digit raises InvalidNSIDError."""
172 nsid_str = "1com.example.recordName"
173+174 with pytest.raises(InvalidNSIDError, match="invalid format"):
175 NSID(nsid_str)
176177 def test_invalid_nsid_name_empty(self):
178 """Test that an NSID with an empty name raises InvalidNSIDError."""
179 nsid_str = "com.example."
180+181 with pytest.raises(InvalidNSIDError, match="invalid format"):
182 NSID(nsid_str)
183184 def test_invalid_nsid_name_too_long(self):
185 """Test that an NSID with a name that is too long raises InvalidNSIDError."""
186 nsid_str = f"com.example.{'a' * 64}"
187+188 with pytest.raises(InvalidNSIDError, match="name length error"):
189 NSID(nsid_str)
190191 def test_invalid_nsid_name_invalid_characters(self):
192 """Test that an NSID with invalid characters in name raises InvalidNSIDError."""
193 nsid_str = "com.example.record-name"
194+195 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
196 NSID(nsid_str)
197198 def test_invalid_nsid_name_starts_with_digit(self):
199 """Test that an NSID with a name starting with a digit raises InvalidNSIDError."""
200 nsid_str = "com.example.1record"
201+202 with pytest.raises(InvalidNSIDError, match="invalid format"):
203 NSID(nsid_str)
204205 def test_invalid_nsid_fragment_empty(self):
206 """Test that an NSID with an empty fragment raises InvalidNSIDError."""
207 nsid_str = "com.example.recordName#"
208+209 with pytest.raises(InvalidNSIDError, match="fragment length error"):
210 NSID(nsid_str)
211212 def test_invalid_nsid_fragment_too_long(self):
213 """Test that an NSID with a fragment that is too long raises InvalidNSIDError."""
214 nsid_str = f"com.example.recordName#{'a' * 64}"
215+216 with pytest.raises(InvalidNSIDError, match="fragment length error"):
217 NSID(nsid_str)
218219 def test_invalid_nsid_fragment_invalid_characters(self):
220 """Test that an NSID with invalid characters in fragment raises InvalidNSIDError."""
221 nsid_str = "com.example.recordName#fragment-with-hyphen"
222+223 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
224 NSID(nsid_str)
225226 def test_invalid_nsid_fragment_starts_with_digit(self):
227 """Test that an NSID with a fragment starting with a digit raises InvalidNSIDError."""
228 nsid_str = "com.example.recordName#1fragment"
229+230 with pytest.raises(InvalidNSIDError, match="invalid format"):
231 NSID(nsid_str)
232···235 nsid_str = "com.example.recordName"
236 nsid1 = NSID(nsid_str)
237 nsid2 = NSID(nsid_str)
238+239 assert nsid1 == nsid2
240 assert nsid1 != "not an nsid object"
241···243 """Test NSID string representation."""
244 nsid_str = "com.example.recordName"
245 nsid = NSID(nsid_str)
246+247 assert str(nsid) == nsid_str
248249 def test_nsid_string_representation_with_fragment(self):
250 """Test NSID string representation with fragment."""
251 nsid_str = "com.example.recordName#fragment"
252 nsid = NSID(nsid_str)
253+254+ assert str(nsid) == nsid_str
+27-17
tests/uri/test_restricted_uri.py
···1011 def test_valid_restricted_uri_with_did_collection_and_rkey(self):
12 """Test creating a RestrictedURI with a valid DID, collection, and rkey."""
13- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
0014 uri = RestrictedURI(uri_str)
15-16 assert str(uri) == uri_str
17 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
18 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···26 """Test creating a RestrictedURI with a valid handle, collection, and rkey."""
27 uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
28 uri = RestrictedURI(uri_str)
29-30 assert str(uri) == uri_str
31 assert uri.authorityAsText == "bnewbold.bsky.team"
32 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···39 """Test creating a RestrictedURI with only a collection."""
40 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
41 uri = RestrictedURI(uri_str)
42-43 assert str(uri) == uri_str
44 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
45 assert uri.path == ["app.bsky.feed.post"]
···51 """Test creating a RestrictedURI with only an authority."""
52 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
53 uri = RestrictedURI(uri_str)
54-55 assert str(uri) == uri_str
56 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
57 assert uri.path == []
···6061 def test_invalid_restricted_uri_with_query(self):
62 """Test that a RestrictedURI with query parameters raises InvalidRestrictedURIError."""
63- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1"
64-65- with pytest.raises(InvalidRestrictedURIError, match="query parameters not supported"):
000066 RestrictedURI(uri_str)
6768 def test_invalid_restricted_uri_with_fragment(self):
69 """Test that a RestrictedURI with a fragment raises InvalidRestrictedURIError."""
70 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
71-72 with pytest.raises(InvalidRestrictedURIError, match="fragments not supported"):
73 RestrictedURI(uri_str)
7475 def test_invalid_restricted_uri_with_invalid_authority(self):
76 """Test that a RestrictedURI with invalid authority raises InvalidRestrictedURIError."""
77 uri_str = "at://invalid_authority/app.bsky.feed.post/3jwdwj2ctlk26"
78-79 with pytest.raises(InvalidRestrictedURIError, match="invalid authority"):
80 RestrictedURI(uri_str)
8182 def test_invalid_restricted_uri_too_many_path_segments(self):
83 """Test that a RestrictedURI with too many path segments raises InvalidRestrictedURIError."""
84 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26/extra"
85-86 with pytest.raises(InvalidRestrictedURIError, match="too many path segments"):
87 RestrictedURI(uri_str)
8889 def test_invalid_restricted_uri_base_uri_validation_failure(self):
90 """Test that a RestrictedURI with invalid base URI raises InvalidURIError."""
91 uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
92-93 with pytest.raises(InvalidURIError, match="invalid format"):
94 RestrictedURI(uri_str)
9596 def test_restricted_uri_equality(self):
97 """Test RestrictedURI equality comparison."""
98- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
0099 uri1 = RestrictedURI(uri_str)
100 uri2 = RestrictedURI(uri_str)
101-102 assert uri1 == uri2
103 assert uri1 != "not a uri object"
104105 def test_restricted_uri_string_representation(self):
106 """Test RestrictedURI string representation."""
107- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
00108 uri = RestrictedURI(uri_str)
109-110- assert str(uri) == uri_str
···1011 def test_valid_restricted_uri_with_did_collection_and_rkey(self):
12 """Test creating a RestrictedURI with a valid DID, collection, and rkey."""
13+ uri_str = (
14+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
15+ )
16 uri = RestrictedURI(uri_str)
17+18 assert str(uri) == uri_str
19 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
20 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···28 """Test creating a RestrictedURI with a valid handle, collection, and rkey."""
29 uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
30 uri = RestrictedURI(uri_str)
31+32 assert str(uri) == uri_str
33 assert uri.authorityAsText == "bnewbold.bsky.team"
34 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···41 """Test creating a RestrictedURI with only a collection."""
42 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
43 uri = RestrictedURI(uri_str)
44+45 assert str(uri) == uri_str
46 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
47 assert uri.path == ["app.bsky.feed.post"]
···53 """Test creating a RestrictedURI with only an authority."""
54 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
55 uri = RestrictedURI(uri_str)
56+57 assert str(uri) == uri_str
58 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
59 assert uri.path == []
···6263 def test_invalid_restricted_uri_with_query(self):
64 """Test that a RestrictedURI with query parameters raises InvalidRestrictedURIError."""
65+ uri_str = (
66+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1"
67+ )
68+69+ with pytest.raises(
70+ InvalidRestrictedURIError, match="query parameters not supported"
71+ ):
72 RestrictedURI(uri_str)
7374 def test_invalid_restricted_uri_with_fragment(self):
75 """Test that a RestrictedURI with a fragment raises InvalidRestrictedURIError."""
76 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
77+78 with pytest.raises(InvalidRestrictedURIError, match="fragments not supported"):
79 RestrictedURI(uri_str)
8081 def test_invalid_restricted_uri_with_invalid_authority(self):
82 """Test that a RestrictedURI with invalid authority raises InvalidRestrictedURIError."""
83 uri_str = "at://invalid_authority/app.bsky.feed.post/3jwdwj2ctlk26"
84+85 with pytest.raises(InvalidRestrictedURIError, match="invalid authority"):
86 RestrictedURI(uri_str)
8788 def test_invalid_restricted_uri_too_many_path_segments(self):
89 """Test that a RestrictedURI with too many path segments raises InvalidRestrictedURIError."""
90 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26/extra"
91+92 with pytest.raises(InvalidRestrictedURIError, match="too many path segments"):
93 RestrictedURI(uri_str)
9495 def test_invalid_restricted_uri_base_uri_validation_failure(self):
96 """Test that a RestrictedURI with invalid base URI raises InvalidURIError."""
97 uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
98+99 with pytest.raises(InvalidURIError, match="invalid format"):
100 RestrictedURI(uri_str)
101102 def test_restricted_uri_equality(self):
103 """Test RestrictedURI equality comparison."""
104+ uri_str = (
105+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
106+ )
107 uri1 = RestrictedURI(uri_str)
108 uri2 = RestrictedURI(uri_str)
109+110 assert uri1 == uri2
111 assert uri1 != "not a uri object"
112113 def test_restricted_uri_string_representation(self):
114 """Test RestrictedURI string representation."""
115+ uri_str = (
116+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
117+ )
118 uri = RestrictedURI(uri_str)
119+120+ assert str(uri) == uri_str
+39-39
tests/uri/test_rkey.py
···13 """Test creating an RKey with a valid simple format."""
14 rkey_str = "3jui7kd54zh2y"
15 rkey = RKey(rkey_str)
16-17 assert str(rkey) == rkey_str
18 assert rkey.recordKey == rkey_str
19···21 """Test creating an RKey with various valid characters."""
22 rkey_str = "example.com"
23 rkey = RKey(rkey_str)
24-25 assert str(rkey) == rkey_str
26 assert rkey.recordKey == rkey_str
27···29 """Test creating an RKey with valid special characters."""
30 rkey_str = "~1.2-3_"
31 rkey = RKey(rkey_str)
32-33 assert str(rkey) == rkey_str
34 assert rkey.recordKey == rkey_str
35···37 """Test creating an RKey with a colon."""
38 rkey_str = "pre:fix"
39 rkey = RKey(rkey_str)
40-41 assert str(rkey) == rkey_str
42 assert rkey.recordKey == rkey_str
43···45 """Test creating an RKey with just an underscore."""
46 rkey_str = "_"
47 rkey = RKey(rkey_str)
48-49 assert str(rkey) == rkey_str
50 assert rkey.recordKey == rkey_str
5152 def test_invalid_rkey_empty(self):
53 """Test that an empty RKey raises InvalidRKeyError."""
54 rkey_str = ""
55-56 with pytest.raises(InvalidRKeyError, match="record key is empty"):
57 RKey(rkey_str)
58···60 """Test that an RKey that is too long raises InvalidRKeyError."""
61 # Create an RKey that exceeds the 512 character limit
62 rkey_str = "a" * 513
63-64 with pytest.raises(InvalidRKeyError, match="exceeds maximum length"):
65 RKey(rkey_str)
6667 def test_invalid_rkey_reserved_double_dot(self):
68 """Test that an RKey with '..' raises InvalidRKeyError."""
69 rkey_str = ".."
70-71 with pytest.raises(InvalidRKeyError, match="reserved value"):
72 RKey(rkey_str)
7374 def test_invalid_rkey_reserved_single_dot(self):
75 """Test that an RKey with '.' raises InvalidRKeyError."""
76 rkey_str = "."
77-78 with pytest.raises(InvalidRKeyError, match="reserved value"):
79 RKey(rkey_str)
8081 def test_invalid_rkey_invalid_characters(self):
82 """Test that an RKey with invalid characters raises InvalidRKeyError."""
83 rkey_str = "alpha/beta"
84-85 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
86 RKey(rkey_str)
8788 def test_invalid_rkey_hash_character(self):
89 """Test that an RKey with a hash character raises InvalidRKeyError."""
90 rkey_str = "#extra"
91-92 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
93 RKey(rkey_str)
9495 def test_invalid_rkey_at_character(self):
96 """Test that an RKey with an at character raises InvalidRKeyError."""
97 rkey_str = "@handle"
98-99 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
100 RKey(rkey_str)
101102 def test_invalid_rkey_space(self):
103 """Test that an RKey with a space raises InvalidRKeyError."""
104 rkey_str = "any space"
105-106 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
107 RKey(rkey_str)
108109 def test_invalid_rkey_plus_character(self):
110 """Test that an RKey with a plus character raises InvalidRKeyError."""
111 rkey_str = "any+space"
112-113 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
114 RKey(rkey_str)
115116 def test_invalid_rkey_brackets(self):
117 """Test that an RKey with brackets raises InvalidRKeyError."""
118 rkey_str = "number[3]"
119-120 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
121 RKey(rkey_str)
122123 def test_invalid_rkey_parentheses(self):
124 """Test that an RKey with parentheses raises InvalidRKeyError."""
125 rkey_str = "number(3)"
126-127 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
128 RKey(rkey_str)
129130 def test_invalid_rkey_quotes(self):
131 """Test that an RKey with quotes raises InvalidRKeyError."""
132 rkey_str = '"quote"'
133-134 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
135 RKey(rkey_str)
136137 def test_invalid_rkey_base64_padding(self):
138 """Test that an RKey with base64 padding raises InvalidRKeyError."""
139 rkey_str = "dHJ1ZQ=="
140-141 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
142 RKey(rkey_str)
143···146 rkey_str = "3jui7kd54zh2y"
147 rkey1 = RKey(rkey_str)
148 rkey2 = RKey(rkey_str)
149-150 assert rkey1 == rkey2
151 assert rkey1 != "not an rkey object"
152···154 """Test RKey string representation."""
155 rkey_str = "3jui7kd54zh2y"
156 rkey = RKey(rkey_str)
157-158 assert str(rkey) == rkey_str
159160···164 def test_tid_creation_default(self):
165 """Test creating a TID with default parameters."""
166 tid = TID()
167-168 assert isinstance(tid, TID)
169 assert isinstance(tid, RKey)
170 assert isinstance(tid.timestamp, datetime.datetime)
···176 """Test creating a TID with a specific timestamp."""
177 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
178 tid = TID(time=timestamp)
179-180 assert tid.timestamp == timestamp
181 assert isinstance(tid.clockIdentifier, int)
182 assert 0 <= tid.clockIdentifier < 1024
···185 """Test creating a TID with a specific clock identifier."""
186 clock_id = 42
187 tid = TID(clockIdentifier=clock_id)
188-189 assert tid.clockIdentifier == clock_id
190 assert isinstance(tid.timestamp, datetime.datetime)
191···194 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
195 clock_id = 42
196 tid = TID(time=timestamp, clockIdentifier=clock_id)
197-198 assert tid.timestamp == timestamp
199 assert tid.clockIdentifier == clock_id
200···203 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
204 clock_id = 42
205 tid = TID(time=timestamp, clockIdentifier=clock_id)
206-207 int_value = int(tid)
208 expected_value = int(timestamp.timestamp() * 1000000) * 1024 + clock_id
209-210 assert int_value == expected_value
211212 def test_tid_string_representation(self):
213 """Test TID string representation."""
214 tid = TID()
215-216 str_value = str(tid)
217 assert len(str_value) == 13
218 assert all(c in "234567abcdefghijklmnopqrstuvwxyz" for c in str_value)
···223 clock_id = 42
224 tid1 = TID(time=timestamp, clockIdentifier=clock_id)
225 tid2 = TID(time=timestamp, clockIdentifier=clock_id)
226-227 assert tid1 == tid2
228229 def test_tid_equality_with_rkey(self):
···232 clock_id = 42
233 tid = TID(time=timestamp, clockIdentifier=clock_id)
234 rkey = RKey(str(tid))
235-236 assert tid == rkey
237238 def test_tid_inequality_with_different_object(self):
239 """Test TID inequality comparison with a different object type."""
240 tid = TID()
241-242 assert tid != "not a tid object"
243244 def test_tid_inequality_with_different_timestamp(self):
···248 clock_id = 42
249 tid1 = TID(time=timestamp1, clockIdentifier=clock_id)
250 tid2 = TID(time=timestamp2, clockIdentifier=clock_id)
251-252 assert tid1 != tid2
253254 def test_tid_inequality_with_different_clock_id(self):
···258 clock_id2 = 43
259 tid1 = TID(time=timestamp, clockIdentifier=clock_id1)
260 tid2 = TID(time=timestamp, clockIdentifier=clock_id2)
261-262 assert tid1 != tid2
263264···268 def test_import_tid_from_integer_default(self):
269 """Test importing a TID from integer with default value."""
270 tid = importTIDfromInteger()
271-272 assert isinstance(tid, TID)
273 assert isinstance(tid.timestamp, datetime.datetime)
274 assert isinstance(tid.clockIdentifier, int)
···280 clock_id = 42
281 original_tid = TID(time=timestamp, clockIdentifier=clock_id)
282 int_value = int(original_tid)
283-284 imported_tid = importTIDfromInteger(int_value)
285-286 assert imported_tid.timestamp == timestamp
287 assert imported_tid.clockIdentifier == clock_id
288289 def test_import_tid_from_base32_default(self):
290 """Test importing a TID from base32 with default value."""
291 tid = importTIDfromBase32()
292-293 assert isinstance(tid, TID)
294 assert isinstance(tid.timestamp, datetime.datetime)
295 assert isinstance(tid.clockIdentifier, int)
···299 """Test importing a TID from base32 with a specific value."""
300 original_tid = TID()
301 str_value = str(original_tid)
302-303 imported_tid = importTIDfromBase32(str_value)
304-305- assert int(imported_tid) == int(original_tid)
···13 """Test creating an RKey with a valid simple format."""
14 rkey_str = "3jui7kd54zh2y"
15 rkey = RKey(rkey_str)
16+17 assert str(rkey) == rkey_str
18 assert rkey.recordKey == rkey_str
19···21 """Test creating an RKey with various valid characters."""
22 rkey_str = "example.com"
23 rkey = RKey(rkey_str)
24+25 assert str(rkey) == rkey_str
26 assert rkey.recordKey == rkey_str
27···29 """Test creating an RKey with valid special characters."""
30 rkey_str = "~1.2-3_"
31 rkey = RKey(rkey_str)
32+33 assert str(rkey) == rkey_str
34 assert rkey.recordKey == rkey_str
35···37 """Test creating an RKey with a colon."""
38 rkey_str = "pre:fix"
39 rkey = RKey(rkey_str)
40+41 assert str(rkey) == rkey_str
42 assert rkey.recordKey == rkey_str
43···45 """Test creating an RKey with just an underscore."""
46 rkey_str = "_"
47 rkey = RKey(rkey_str)
48+49 assert str(rkey) == rkey_str
50 assert rkey.recordKey == rkey_str
5152 def test_invalid_rkey_empty(self):
53 """Test that an empty RKey raises InvalidRKeyError."""
54 rkey_str = ""
55+56 with pytest.raises(InvalidRKeyError, match="record key is empty"):
57 RKey(rkey_str)
58···60 """Test that an RKey that is too long raises InvalidRKeyError."""
61 # Create an RKey that exceeds the 512 character limit
62 rkey_str = "a" * 513
63+64 with pytest.raises(InvalidRKeyError, match="exceeds maximum length"):
65 RKey(rkey_str)
6667 def test_invalid_rkey_reserved_double_dot(self):
68 """Test that an RKey with '..' raises InvalidRKeyError."""
69 rkey_str = ".."
70+71 with pytest.raises(InvalidRKeyError, match="reserved value"):
72 RKey(rkey_str)
7374 def test_invalid_rkey_reserved_single_dot(self):
75 """Test that an RKey with '.' raises InvalidRKeyError."""
76 rkey_str = "."
77+78 with pytest.raises(InvalidRKeyError, match="reserved value"):
79 RKey(rkey_str)
8081 def test_invalid_rkey_invalid_characters(self):
82 """Test that an RKey with invalid characters raises InvalidRKeyError."""
83 rkey_str = "alpha/beta"
84+85 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
86 RKey(rkey_str)
8788 def test_invalid_rkey_hash_character(self):
89 """Test that an RKey with a hash character raises InvalidRKeyError."""
90 rkey_str = "#extra"
91+92 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
93 RKey(rkey_str)
9495 def test_invalid_rkey_at_character(self):
96 """Test that an RKey with an at character raises InvalidRKeyError."""
97 rkey_str = "@handle"
98+99 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
100 RKey(rkey_str)
101102 def test_invalid_rkey_space(self):
103 """Test that an RKey with a space raises InvalidRKeyError."""
104 rkey_str = "any space"
105+106 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
107 RKey(rkey_str)
108109 def test_invalid_rkey_plus_character(self):
110 """Test that an RKey with a plus character raises InvalidRKeyError."""
111 rkey_str = "any+space"
112+113 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
114 RKey(rkey_str)
115116 def test_invalid_rkey_brackets(self):
117 """Test that an RKey with brackets raises InvalidRKeyError."""
118 rkey_str = "number[3]"
119+120 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
121 RKey(rkey_str)
122123 def test_invalid_rkey_parentheses(self):
124 """Test that an RKey with parentheses raises InvalidRKeyError."""
125 rkey_str = "number(3)"
126+127 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
128 RKey(rkey_str)
129130 def test_invalid_rkey_quotes(self):
131 """Test that an RKey with quotes raises InvalidRKeyError."""
132 rkey_str = '"quote"'
133+134 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
135 RKey(rkey_str)
136137 def test_invalid_rkey_base64_padding(self):
138 """Test that an RKey with base64 padding raises InvalidRKeyError."""
139 rkey_str = "dHJ1ZQ=="
140+141 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
142 RKey(rkey_str)
143···146 rkey_str = "3jui7kd54zh2y"
147 rkey1 = RKey(rkey_str)
148 rkey2 = RKey(rkey_str)
149+150 assert rkey1 == rkey2
151 assert rkey1 != "not an rkey object"
152···154 """Test RKey string representation."""
155 rkey_str = "3jui7kd54zh2y"
156 rkey = RKey(rkey_str)
157+158 assert str(rkey) == rkey_str
159160···164 def test_tid_creation_default(self):
165 """Test creating a TID with default parameters."""
166 tid = TID()
167+168 assert isinstance(tid, TID)
169 assert isinstance(tid, RKey)
170 assert isinstance(tid.timestamp, datetime.datetime)
···176 """Test creating a TID with a specific timestamp."""
177 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
178 tid = TID(time=timestamp)
179+180 assert tid.timestamp == timestamp
181 assert isinstance(tid.clockIdentifier, int)
182 assert 0 <= tid.clockIdentifier < 1024
···185 """Test creating a TID with a specific clock identifier."""
186 clock_id = 42
187 tid = TID(clockIdentifier=clock_id)
188+189 assert tid.clockIdentifier == clock_id
190 assert isinstance(tid.timestamp, datetime.datetime)
191···194 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
195 clock_id = 42
196 tid = TID(time=timestamp, clockIdentifier=clock_id)
197+198 assert tid.timestamp == timestamp
199 assert tid.clockIdentifier == clock_id
200···203 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
204 clock_id = 42
205 tid = TID(time=timestamp, clockIdentifier=clock_id)
206+207 int_value = int(tid)
208 expected_value = int(timestamp.timestamp() * 1000000) * 1024 + clock_id
209+210 assert int_value == expected_value
211212 def test_tid_string_representation(self):
213 """Test TID string representation."""
214 tid = TID()
215+216 str_value = str(tid)
217 assert len(str_value) == 13
218 assert all(c in "234567abcdefghijklmnopqrstuvwxyz" for c in str_value)
···223 clock_id = 42
224 tid1 = TID(time=timestamp, clockIdentifier=clock_id)
225 tid2 = TID(time=timestamp, clockIdentifier=clock_id)
226+227 assert tid1 == tid2
228229 def test_tid_equality_with_rkey(self):
···232 clock_id = 42
233 tid = TID(time=timestamp, clockIdentifier=clock_id)
234 rkey = RKey(str(tid))
235+236 assert tid == rkey
237238 def test_tid_inequality_with_different_object(self):
239 """Test TID inequality comparison with a different object type."""
240 tid = TID()
241+242 assert tid != "not a tid object"
243244 def test_tid_inequality_with_different_timestamp(self):
···248 clock_id = 42
249 tid1 = TID(time=timestamp1, clockIdentifier=clock_id)
250 tid2 = TID(time=timestamp2, clockIdentifier=clock_id)
251+252 assert tid1 != tid2
253254 def test_tid_inequality_with_different_clock_id(self):
···258 clock_id2 = 43
259 tid1 = TID(time=timestamp, clockIdentifier=clock_id1)
260 tid2 = TID(time=timestamp, clockIdentifier=clock_id2)
261+262 assert tid1 != tid2
263264···268 def test_import_tid_from_integer_default(self):
269 """Test importing a TID from integer with default value."""
270 tid = importTIDfromInteger()
271+272 assert isinstance(tid, TID)
273 assert isinstance(tid.timestamp, datetime.datetime)
274 assert isinstance(tid.clockIdentifier, int)
···280 clock_id = 42
281 original_tid = TID(time=timestamp, clockIdentifier=clock_id)
282 int_value = int(original_tid)
283+284 imported_tid = importTIDfromInteger(int_value)
285+286 assert imported_tid.timestamp == timestamp
287 assert imported_tid.clockIdentifier == clock_id
288289 def test_import_tid_from_base32_default(self):
290 """Test importing a TID from base32 with default value."""
291 tid = importTIDfromBase32()
292+293 assert isinstance(tid, TID)
294 assert isinstance(tid.timestamp, datetime.datetime)
295 assert isinstance(tid.clockIdentifier, int)
···299 """Test importing a TID from base32 with a specific value."""
300 original_tid = TID()
301 str_value = str(original_tid)
302+303 imported_tid = importTIDfromBase32(str_value)
304+305+ assert int(imported_tid) == int(original_tid)
+26-18
tests/uri/test_uri.py
···1011 def test_valid_uri_with_did(self):
12 """Test creating a URI with a valid DID."""
13- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
0014 uri = URI(uri_str)
15-16 assert str(uri) == uri_str
17 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
18 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···26 """Test creating a URI with a valid handle."""
27 uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
28 uri = URI(uri_str)
29-30 assert str(uri) == uri_str
31 assert uri.authorityAsText == "bnewbold.bsky.team"
32 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···36 """Test creating a URI with only a collection."""
37 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
38 uri = URI(uri_str)
39-40 assert str(uri) == uri_str
41 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
42 assert uri.path == ["app.bsky.feed.post"]
···46 """Test creating a URI with only an authority."""
47 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
48 uri = URI(uri_str)
49-50 assert str(uri) == uri_str
51 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
52 assert uri.path == []
···56 """Test creating a URI with query parameters."""
57 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1¶m2=value2"
58 uri = URI(uri_str)
59-60 assert uri.query == {"param1": ["value1"], "param2": ["value2"]}
61 assert uri.queryAsText == "param1%3Dvalue1%26param2%3Dvalue2"
62···64 """Test creating a URI with a fragment."""
65 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
66 uri = URI(uri_str)
67-68 assert uri.fragment is not None
69 assert uri.fragmentAsText == "%24.some.json.path"
7071 def test_invalid_uri_non_ascii_characters(self):
72 """Test that non-ASCII characters in URI raise InvalidURIError."""
73 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/💩"
74-75 with pytest.raises(InvalidURIError, match="contains invalid characters"):
76 URI(uri_str)
77···80 # Create a URI that exceeds the 8000 character limit
81 long_path = "a" * 8000
82 uri_str = f"at://did:plc:z72i7hdynmk6r22z27h6tvur/{long_path}"
83-84 with pytest.raises(InvalidURIError, match="exceeds maximum length"):
85 URI(uri_str)
8687 def test_invalid_uri_wrong_scheme(self):
88 """Test that a URI with wrong scheme raises InvalidURIError."""
89- uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
90-0091 with pytest.raises(InvalidURIError, match="invalid format"):
92 URI(uri_str)
9394 def test_invalid_uri_trailing_slash(self):
95 """Test that a URI with trailing slash raises InvalidURIError."""
96 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/"
97-98 with pytest.raises(InvalidURIError, match="cannot end with a slash"):
99 URI(uri_str)
100101 def test_invalid_uri_with_userinfo(self):
102 """Test that a URI with userinfo raises InvalidURIError."""
103 uri_str = "at://user:pass@did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
104-105 with pytest.raises(InvalidURIError, match="does not support user information"):
106 URI(uri_str)
107108 def test_uri_equality(self):
109 """Test URI equality comparison."""
110- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
00111 uri1 = URI(uri_str)
112 uri2 = URI(uri_str)
113-114 assert uri1 == uri2
115 assert uri1 != "not a uri object"
116117 def test_uri_string_representation(self):
118 """Test URI string representation."""
119- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
00120 uri = URI(uri_str)
121-122- assert str(uri) == uri_str
···1011 def test_valid_uri_with_did(self):
12 """Test creating a URI with a valid DID."""
13+ uri_str = (
14+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
15+ )
16 uri = URI(uri_str)
17+18 assert str(uri) == uri_str
19 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
20 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···28 """Test creating a URI with a valid handle."""
29 uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
30 uri = URI(uri_str)
31+32 assert str(uri) == uri_str
33 assert uri.authorityAsText == "bnewbold.bsky.team"
34 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···38 """Test creating a URI with only a collection."""
39 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
40 uri = URI(uri_str)
41+42 assert str(uri) == uri_str
43 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
44 assert uri.path == ["app.bsky.feed.post"]
···48 """Test creating a URI with only an authority."""
49 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
50 uri = URI(uri_str)
51+52 assert str(uri) == uri_str
53 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
54 assert uri.path == []
···58 """Test creating a URI with query parameters."""
59 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1¶m2=value2"
60 uri = URI(uri_str)
61+62 assert uri.query == {"param1": ["value1"], "param2": ["value2"]}
63 assert uri.queryAsText == "param1%3Dvalue1%26param2%3Dvalue2"
64···66 """Test creating a URI with a fragment."""
67 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
68 uri = URI(uri_str)
69+70 assert uri.fragment is not None
71 assert uri.fragmentAsText == "%24.some.json.path"
7273 def test_invalid_uri_non_ascii_characters(self):
74 """Test that non-ASCII characters in URI raise InvalidURIError."""
75 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/💩"
76+77 with pytest.raises(InvalidURIError, match="contains invalid characters"):
78 URI(uri_str)
79···82 # Create a URI that exceeds the 8000 character limit
83 long_path = "a" * 8000
84 uri_str = f"at://did:plc:z72i7hdynmk6r22z27h6tvur/{long_path}"
85+86 with pytest.raises(InvalidURIError, match="exceeds maximum length"):
87 URI(uri_str)
8889 def test_invalid_uri_wrong_scheme(self):
90 """Test that a URI with wrong scheme raises InvalidURIError."""
91+ uri_str = (
92+ "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
93+ )
94+95 with pytest.raises(InvalidURIError, match="invalid format"):
96 URI(uri_str)
9798 def test_invalid_uri_trailing_slash(self):
99 """Test that a URI with trailing slash raises InvalidURIError."""
100 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/"
101+102 with pytest.raises(InvalidURIError, match="cannot end with a slash"):
103 URI(uri_str)
104105 def test_invalid_uri_with_userinfo(self):
106 """Test that a URI with userinfo raises InvalidURIError."""
107 uri_str = "at://user:pass@did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
108+109 with pytest.raises(InvalidURIError, match="does not support user information"):
110 URI(uri_str)
111112 def test_uri_equality(self):
113 """Test URI equality comparison."""
114+ uri_str = (
115+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
116+ )
117 uri1 = URI(uri_str)
118 uri2 = URI(uri_str)
119+120 assert uri1 == uri2
121 assert uri1 != "not a uri object"
122123 def test_uri_string_representation(self):
124 """Test URI string representation."""
125+ uri_str = (
126+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
127+ )
128 uri = URI(uri_str)
129+130+ assert str(uri) == uri_str