···13131414class JsonDecoder(json.JSONDecoder):
1515 """A JSON decoder that supports ATProto data types.
1616-1616+1717 This decoder extends the standard JSON decoder to handle ATProto-specific
1818 data types, including bytes, CID links, and typed objects.
1919-1919+2020 Attributes:
2121 type_hook_registry: Registry for type-specific hooks.
2222 encoding: The encoding to use for string deserialization.
2323 """
2424-2424+2525 def __init__(
2626 self,
2727 *,
···2929 type_hook_registry: Optional[Any] = None,
3030 type_processor_registry: Optional[Any] = None,
3131 encoding: str = "utf-8",
3232- **kwargs: Any
3232+ **kwargs: Any,
3333 ) -> None:
3434 """Initialize the JSON decoder.
3535-3535+3636 Args:
3737 object_hook: Optional function to call with each decoded object.
3838 type_hook_registry: Registry for type-specific hooks.
···4545 type_hook_registry = type_processor_registry.to_hook_registry()
4646 elif type_hook_registry is None:
4747 from .hooks import get_global_registry
4848+4849 type_hook_registry = get_global_registry()
4949-5050+5051 # Create a combined object hook that calls both the custom hook and our hook
5152 combined_hook = self._create_combined_hook(object_hook, type_hook_registry)
5252-5353+5354 super().__init__(object_hook=combined_hook, **kwargs)
5455 self.type_hook_registry = type_hook_registry
5556 self.type_processor_registry = type_processor_registry
5657 self.encoding = encoding
5757-5858+5859 def _create_combined_hook(
5960 self,
6061 custom_hook: Optional[Callable[[Dict[str, Any]], Any]],
6161- type_hook_registry: Optional[Any]
6262+ type_hook_registry: Optional[Any],
6263 ) -> Callable[[Dict[str, Any]], Any]:
6364 """Create a combined object hook function.
6464-6565+6566 Args:
6667 custom_hook: Optional custom object hook function.
6768 type_hook_registry: Registry for type-specific hooks.
6868-6969+6970 Returns:
7071 A combined object hook function.
7172 """
7373+7274 def combined_hook(obj: Dict[str, Any]) -> Any:
7375 # First, apply our ATProto-specific decoding
7476 decoded_obj = self._atproto_object_hook(obj)
7575-7777+7678 # Then, apply the custom hook if provided
7779 if custom_hook is not None:
7880 decoded_obj = custom_hook(decoded_obj)
7979-8181+8082 return decoded_obj
8181-8383+8284 return combined_hook
8383-8585+8486 def _atproto_object_hook(self, obj: Dict[str, Any]) -> Any:
8587 """Handle ATProto-specific object decoding.
8686-8888+8789 Args:
8890 obj: The object to decode.
8989-9191+9092 Returns:
9193 The decoded object.
9294 """
···9698 # If there are other keys, this is invalid
9799 raise ValueError(f"Invalid $bytes object: {obj}")
98100 return base64.b64decode(obj["$bytes"].encode(self.encoding))
9999-101101+100102 # Handle $link key (CID parsing)
101103 elif "$link" in obj:
102104 if len(obj) != 1:
103105 # If there are other keys, this is invalid
104106 raise ValueError(f"Invalid $link object: {obj}")
105107 return make_cid(obj["$link"])
106106-108108+107109 # Handle $type key (typed objects)
108110 elif "$type" in obj:
109111 type_value = obj["$type"]
110112 remaining_obj = {k: v for k, v in obj.items() if k != "$type"}
111111-113113+112114 # Check if there's a registered type handler
113115 if self.type_hook_registry is not None:
114116 handler = self.type_hook_registry.get_handler(type_value)
115117 if handler is not None:
116118 return handler(remaining_obj)
117117-119119+118120 # If no handler is registered, return a typed object
119121 return TypedObject(type_value, remaining_obj)
120120-122122+121123 # Handle nested objects recursively
122124 elif isinstance(obj, dict):
123123- return {k: self._atproto_object_hook(v) if isinstance(v, dict) else v
124124- for k, v in obj.items()}
125125-125125+ return {
126126+ k: self._atproto_object_hook(v) if isinstance(v, dict) else v
127127+ for k, v in obj.items()
128128+ }
129129+126130 return obj
127131128132129133class TypedObject:
130134 """A typed object in the ATProto data model.
131131-135135+132136 This class represents an object with a $type field in the ATProto data model.
133133-137137+134138 Attributes:
135139 type: The type of the object.
136140 data: The data associated with the object.
137141 """
138138-142142+139143 def __init__(self, type_name: str, data: Dict[str, Any]) -> None:
140144 """Initialize a typed object.
141141-145145+142146 Args:
143147 type_name: The type of the object.
144148 data: The data associated with the object.
145149 """
146150 self.type_name = type_name
147151 self.data = data
148148-152152+149153 def __repr__(self) -> str:
150154 """Return a string representation of the typed object.
151151-155155+152156 Returns:
153157 A string representation of the typed object.
154158 """
155159 return f"TypedObject(type_name={self.type_name!r}, data={self.data!r})"
156156-160160+157161 def __eq__(self, other: Any) -> bool:
158162 """Check if two typed objects are equal.
159159-163163+160164 Args:
161165 other: The object to compare with.
162162-166166+163167 Returns:
164168 True if the objects are equal, False otherwise.
165169 """
166170 if not isinstance(other, TypedObject):
167171 return False
168172 return self.type_name == other.type_name and self.data == other.data
169169-173173+170174 def __atproto_json_encode__(self) -> Dict[str, Any]:
171175 """Encode the typed object to a JSON-serializable format.
172172-176176+173177 Returns:
174178 A JSON-serializable representation of the typed object.
175179 """
176180 result = {"$type": self.type_name}
177181 result.update(self.data)
178178- return result182182+ return result
+10-10
src/atpasser/data/encoder.py
···13131414class JsonEncoder(json.JSONEncoder):
1515 """A JSON encoder that supports ATProto data types.
1616-1616+1717 This encoder extends the standard JSON encoder to handle ATProto-specific
1818 data types, including bytes, CID links, and typed objects.
1919-1919+2020 Attributes:
2121 encoding (str): The encoding to use for string serialization.
2222 type_processor_registry: Registry for type-specific processors.
2323 """
2424-2424+2525 def __init__(
2626 self,
2727 *,
2828 encoding: str = "utf-8",
2929 type_processor_registry: Optional[Any] = None,
3030- **kwargs: Any
3030+ **kwargs: Any,
3131 ) -> None:
3232 """Initialize the JSON encoder.
3333-3333+3434 Args:
3535 encoding: The encoding to use for string serialization.
3636 type_processor_registry: Registry for type-specific processors.
···3939 super().__init__(**kwargs)
4040 self.encoding = encoding
4141 self.type_processor_registry = type_processor_registry
4242-4242+4343 def default(self, o: Any) -> Any:
4444 """Convert an object to a serializable format.
4545-4545+4646 Args:
4747 o: The object to serialize.
4848-4848+4949 Returns:
5050 A serializable representation of the object.
5151-5151+5252 Raises:
5353 TypeError: If the object is not serializable.
5454 """
···7979 return [self.default(item) for item in o]
8080 else:
8181 # Use the parent class for other types
8282- return super().default(o)8282+ return super().default(o)
+51-46
src/atpasser/data/hooks.py
···99from typing import Any, Callable, Dict, Optional, TypeVar, Union
10101111# Type variable for the decorated function
1212-F = TypeVar('F', bound=Callable[..., Any])
1212+F = TypeVar("F", bound=Callable[..., Any])
131314141515class TypeHookRegistry:
1616 """Registry for type-specific hooks in the ATProto JSON decoder.
1717-1717+1818 This class maintains a registry of type-specific hooks that can be used
1919 to customize the decoding of objects with $type keys in the ATProto data model.
2020-2020+2121 Attributes:
2222 _handlers: Dictionary mapping type names to handler functions.
2323 """
2424-2424+2525 def __init__(self) -> None:
2626 """Initialize the type hook registry."""
2727 self._handlers: Dict[str, Callable[[Dict[str, Any]], Any]] = {}
2828-2828+2929 def register(self, type_name: str) -> Callable[[F], F]:
3030 """Register a type handler function.
3131-3131+3232 This method can be used as a decorator to register a function as a handler
3333 for a specific type.
3434-3434+3535 Args:
3636 type_name: The name of the type to handle.
3737-3737+3838 Returns:
3939 A decorator function that registers the decorated function as a handler.
4040-4040+4141 Example:
4242 >>> registry = TypeHookRegistry()
4343- >>>
4343+ >>>
4444 >>> @registry.register("app.bsky.feed.post")
4545 ... def handle_post(data: Dict[str, Any]) -> Any:
4646 ... return Post(**data)
4747 """
4848+4849 def decorator(func: F) -> F:
4950 self._handlers[type_name] = func
5051 return func
5151-5252+5253 return decorator
5353-5454- def register_handler(self, type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None:
5454+5555+ def register_handler(
5656+ self, type_name: str, handler: Callable[[Dict[str, Any]], Any]
5757+ ) -> None:
5558 """Register a type handler function directly.
5656-5959+5760 Args:
5861 type_name: The name of the type to handle.
5962 handler: The function to call when decoding objects of this type.
6060-6363+6164 Example:
6265 >>> registry = TypeHookRegistry()
6363- >>>
6666+ >>>
6467 >>> def handle_post(data: Dict[str, Any]) -> Any:
6568 ... return Post(**data)
6666- >>>
6969+ >>>
6770 >>> registry.register_handler("app.bsky.feed.post", handle_post)
6871 """
6972 self._handlers[type_name] = handler
7070-7373+7174 def unregister(self, type_name: str) -> None:
7275 """Unregister a type handler function.
7373-7676+7477 Args:
7578 type_name: The name of the type to unregister.
7679 """
7780 if type_name in self._handlers:
7881 del self._handlers[type_name]
7979-8282+8083 def get_handler(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
8184 """Get the handler function for a specific type.
8282-8585+8386 Args:
8487 type_name: The name of the type to get the handler for.
8585-8888+8689 Returns:
8790 The handler function for the specified type, or None if no handler
8891 is registered.
8992 """
9093 return self._handlers.get(type_name)
9191-9494+9295 def has_handler(self, type_name: str) -> bool:
9396 """Check if a handler is registered for a specific type.
9494-9797+9598 Args:
9699 type_name: The name of the type to check.
9797-100100+98101 Returns:
99102 True if a handler is registered for the specified type, False otherwise.
100103 """
101104 return type_name in self._handlers
102102-105105+103106 def clear(self) -> None:
104107 """Clear all registered handlers."""
105108 self._handlers.clear()
106106-109109+107110 def get_registered_types(self) -> set:
108111 """Get the set of all registered type names.
109109-112112+110113 Returns:
111114 A set of all registered type names.
112115 """
···119122120123def type_handler(type_name: str) -> Callable[[F], F]:
121124 """Register a global type handler function.
122122-125125+123126 This decorator registers a function as a global handler for a specific type
124127 in the ATProto data model.
125125-128128+126129 Args:
127130 type_name: The name of the type to handle.
128128-131131+129132 Returns:
130133 A decorator function that registers the decorated function as a handler.
131131-134134+132135 Example:
133136 >>> @type_handler("app.bsky.feed.post")
134137 ... def handle_post(data: Dict[str, Any]) -> Any:
···139142140143def get_global_registry() -> TypeHookRegistry:
141144 """Get the global type hook registry.
142142-145145+143146 Returns:
144147 The global TypeHookRegistry instance.
145148 """
146149 return _global_registry
147150148151149149-def register_type_handler(type_name: str, handler: Callable[[Dict[str, Any]], Any]) -> None:
152152+def register_type_handler(
153153+ type_name: str, handler: Callable[[Dict[str, Any]], Any]
154154+) -> None:
150155 """Register a global type handler function directly.
151151-156156+152157 Args:
153158 type_name: The name of the type to handle.
154159 handler: The function to call when decoding objects of this type.
155155-160160+156161 Example:
157162 >>> def handle_post(data: Dict[str, Any]) -> Any:
158163 ... return Post(**data)
159159- >>>
164164+ >>>
160165 >>> register_type_handler("app.bsky.feed.post", handle_post)
161166 """
162167 _global_registry.register_handler(type_name, handler)
···164169165170def unregister_type_handler(type_name: str) -> None:
166171 """Unregister a global type handler function.
167167-172172+168173 Args:
169174 type_name: The name of the type to unregister.
170175 """
···173178174179def get_type_handler(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
175180 """Get the global handler function for a specific type.
176176-181181+177182 Args:
178183 type_name: The name of the type to get the handler for.
179179-184184+180185 Returns:
181186 The handler function for the specified type, or None if no handler
182187 is registered.
···186191187192def has_type_handler(type_name: str) -> bool:
188193 """Check if a global handler is registered for a specific type.
189189-194194+190195 Args:
191196 type_name: The name of the type to check.
192192-197197+193198 Returns:
194199 True if a handler is registered for the specified type, False otherwise.
195200 """
···203208204209def get_registered_types() -> set:
205210 """Get the set of all globally registered type names.
206206-211211+207212 Returns:
208213 A set of all registered type names.
209214 """
···212217213218def create_registry() -> TypeHookRegistry:
214219 """Create a new type hook registry.
215215-220220+216221 This function creates a new, independent registry that can be used
217222 instead of the global registry.
218218-223223+219224 Returns:
220225 A new TypeHookRegistry instance.
221226 """
222222- return TypeHookRegistry()227227+ return TypeHookRegistry()
+120-114
src/atpasser/data/types.py
···1010from .hooks import TypeHookRegistry
11111212# Type variable for the decorated class
1313-T = TypeVar('T')
1313+T = TypeVar("T")
141415151616class TypeProcessor:
1717 """A type processor for ATProto JSON objects.
1818-1818+1919 This class represents a processor for a specific type in the ATProto data model.
2020 It contains information about how to convert JSON data to Python objects and
2121 vice versa.
2222-2222+2323 Attributes:
2424 type_name: The name of the type this processor handles.
2525 decoder: The function to decode JSON data to a Python object.
2626 encoder: The function to encode a Python object to JSON data.
2727 priority: The priority of this processor (higher values = higher priority).
2828 """
2929-2929+3030 def __init__(
3131 self,
3232 type_name: str,
3333 decoder: Optional[Callable[[Dict[str, Any]], Any]] = None,
3434 encoder: Optional[Callable[[Any], Dict[str, Any]]] = None,
3535- priority: int = 0
3535+ priority: int = 0,
3636 ) -> None:
3737 """Initialize a type processor.
3838-3838+3939 Args:
4040 type_name: The name of the type this processor handles.
4141 decoder: The function to decode JSON data to a Python object.
···4646 self.decoder = decoder
4747 self.encoder = encoder
4848 self.priority = priority
4949-4949+5050 def decode(self, data: Dict[str, Any]) -> Any:
5151 """Decode JSON data to a Python object.
5252-5252+5353 Args:
5454 data: The JSON data to decode.
5555-5555+5656 Returns:
5757 The decoded Python object.
5858-5858+5959 Raises:
6060 ValueError: If no decoder is registered.
6161 """
6262 if self.decoder is None:
6363 raise ValueError(f"No decoder registered for type {self.type_name}")
6464 return self.decoder(data)
6565-6565+6666 def encode(self, obj: Any) -> Dict[str, Any]:
6767 """Encode a Python object to JSON data.
6868-6868+6969 Args:
7070 obj: The Python object to encode.
7171-7171+7272 Returns:
7373 The encoded JSON data.
7474-7474+7575 Raises:
7676 ValueError: If no encoder is registered.
7777 """
···82828383class TypeProcessorRegistry:
8484 """Registry for type processors in the ATProto JSON decoder.
8585-8585+8686 This class maintains a registry of type processors that can be used
8787 to customize the encoding and decoding of objects with $type keys in
8888 the ATProto data model.
8989-8989+9090 Attributes:
9191 _processors: Dictionary mapping type names to processor lists.
9292 """
9393-9393+9494 def __init__(self) -> None:
9595 """Initialize the type processor registry."""
9696 self._processors: Dict[str, List[TypeProcessor]] = {}
9797-9797+9898 def register_processor(self, processor: TypeProcessor) -> None:
9999 """Register a type processor.
100100-100100+101101 Args:
102102 processor: The type processor to register.
103103 """
104104 if processor.type_name not in self._processors:
105105 self._processors[processor.type_name] = []
106106-106106+107107 self._processors[processor.type_name].append(processor)
108108 # Sort processors by priority (descending)
109109- self._processors[processor.type_name].sort(key=lambda p: p.priority, reverse=True)
110110-109109+ self._processors[processor.type_name].sort(
110110+ key=lambda p: p.priority, reverse=True
111111+ )
112112+111113 def register(
112112- self,
113113- type_name: str,
114114- priority: int = 0
114114+ self, type_name: str, priority: int = 0
115115 ) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
116116 """Register a type decoder function.
117117-117117+118118 This method can be used as a decorator to register a function as a decoder
119119 for a specific type.
120120-120120+121121 Args:
122122 type_name: The name of the type to handle.
123123 priority: The priority of this processor (higher values = higher priority).
124124-124124+125125 Returns:
126126 A decorator function that registers the decorated function as a decoder.
127127-127127+128128 Example:
129129 >>> registry = TypeProcessorRegistry()
130130- >>>
130130+ >>>
131131 >>> @registry.register("app.bsky.feed.post", priority=10)
132132 ... def decode_post(data: Dict[str, Any]) -> Any:
133133 ... return Post(**data)
134134 """
135135- def decorator(func: Callable[[Dict[str, Any]], Any]) -> Callable[[Dict[str, Any]], Any]:
135135+136136+ def decorator(
137137+ func: Callable[[Dict[str, Any]], Any],
138138+ ) -> Callable[[Dict[str, Any]], Any]:
136139 processor = TypeProcessor(type_name, decoder=func, priority=priority)
137140 self.register_processor(processor)
138141 return func
139139-142142+140143 return decorator
141141-144144+142145 def register_encoder(
143143- self,
144144- type_name: str,
145145- priority: int = 0
146146+ self, type_name: str, priority: int = 0
146147 ) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
147148 """Register a type encoder function.
148148-149149+149150 This method can be used as a decorator to register a function as an encoder
150151 for a specific type.
151151-152152+152153 Args:
153154 type_name: The name of the type to handle.
154155 priority: The priority of this processor (higher values = higher priority).
155155-156156+156157 Returns:
157158 A decorator function that registers the decorated function as an encoder.
158158-159159+159160 Example:
160161 >>> registry = TypeProcessorRegistry()
161161- >>>
162162+ >>>
162163 >>> @registry.register_encoder("app.bsky.feed.post", priority=10)
163164 ... def encode_post(post: Post) -> Dict[str, Any]:
164165 ... return {"text": post.text, "createdAt": post.created_at}
165166 """
166166- def decorator(func: Callable[[Any], Dict[str, Any]]) -> Callable[[Any], Dict[str, Any]]:
167167+168168+ def decorator(
169169+ func: Callable[[Any], Dict[str, Any]],
170170+ ) -> Callable[[Any], Dict[str, Any]]:
167171 # Check if a processor for this type already exists
168172 if type_name in self._processors:
169173 for processor in self._processors[type_name]:
···173177 break
174178 else:
175179 # No decoder found, create a new processor
176176- processor = TypeProcessor(type_name, encoder=func, priority=priority)
180180+ processor = TypeProcessor(
181181+ type_name, encoder=func, priority=priority
182182+ )
177183 self.register_processor(processor)
178184 else:
179185 # No processor exists, create a new one
180186 processor = TypeProcessor(type_name, encoder=func, priority=priority)
181187 self.register_processor(processor)
182182-188188+183189 return func
184184-190190+185191 return decorator
186186-192192+187193 def register_class(
188188- self,
189189- type_name: str,
190190- priority: int = 0
194194+ self, type_name: str, priority: int = 0
191195 ) -> Callable[[Type[T]], Type[T]]:
192196 """Register a class for both encoding and decoding.
193193-197197+194198 This method can be used as a decorator to register a class for both
195199 encoding and decoding of a specific type.
196196-200200+197201 The class must have a class method `from_json` that takes a dictionary
198202 and returns an instance of the class, and an instance method `to_json`
199203 that returns a dictionary.
200200-204204+201205 Args:
202206 type_name: The name of the type to handle.
203207 priority: The priority of this processor (higher values = higher priority).
204204-208208+205209 Returns:
206210 A decorator function that registers the decorated class.
207207-211211+208212 Example:
209213 >>> registry = TypeProcessorRegistry()
210210- >>>
214214+ >>>
211215 >>> @registry.register_class("app.bsky.feed.post", priority=10)
212216 ... class Post:
213217 ... def __init__(self, text: str, created_at: str) -> None:
214218 ... self.text = text
215219 ... self.created_at = created_at
216216- ...
220220+ ...
217221 ... @classmethod
218222 ... def from_json(cls, data: Dict[str, Any]) -> "Post":
219223 ... return cls(data["text"], data["createdAt"])
220220- ...
224224+ ...
221225 ... def to_json(self) -> Dict[str, Any]:
222226 ... return {"text": self.text, "createdAt": self.created_at}
223227 """
228228+224229 def decorator(cls: Type[T]) -> Type[T]:
225230 # Create decoder from class method
226231 if hasattr(cls, "from_json"):
···232237 # Create a decoder that passes the data as keyword arguments
233238 decoder = lambda data: cls(**data)
234239 else:
235235- raise ValueError(f"Class {cls.__name__} has no from_json method or compatible __init__")
236236-240240+ raise ValueError(
241241+ f"Class {cls.__name__} has no from_json method or compatible __init__"
242242+ )
243243+237244 # Create encoder from instance method
238245 if hasattr(cls, "to_json"):
239246 encoder = lambda obj: obj.to_json()
240247 else:
241248 raise ValueError(f"Class {cls.__name__} has no to_json method")
242242-249249+243250 # Register the processor
244244- processor = TypeProcessor(type_name, decoder=decoder, encoder=encoder, priority=priority)
251251+ processor = TypeProcessor(
252252+ type_name, decoder=decoder, encoder=encoder, priority=priority
253253+ )
245254 self.register_processor(processor)
246246-255255+247256 return cls
248248-257257+249258 return decorator
250250-259259+251260 def unregister(self, type_name: str, priority: Optional[int] = None) -> None:
252261 """Unregister type processors.
253253-262262+254263 Args:
255264 type_name: The name of the type to unregister.
256265 priority: If specified, only unregister processors with this priority.
···264273 else:
265274 # Remove all processors for this type
266275 del self._processors[type_name]
267267-276276+268277 def get_decoder(self, type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
269278 """Get the decoder function for a specific type.
270270-279279+271280 Args:
272281 type_name: The name of the type to get the decoder for.
273273-282282+274283 Returns:
275284 The decoder function for the specified type, or None if no decoder
276285 is registered.
···279288 # Return the decoder of the highest priority processor
280289 return self._processors[type_name][0].decoder
281290 return None
282282-291291+283292 def get_encoder(self, type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
284293 """Get the encoder function for a specific type.
285285-294294+286295 Args:
287296 type_name: The name of the type to get the encoder for.
288288-297297+289298 Returns:
290299 The encoder function for the specified type, or None if no encoder
291300 is registered.
···294303 # Return the encoder of the highest priority processor
295304 return self._processors[type_name][0].encoder
296305 return None
297297-306306+298307 def has_processor(self, type_name: str) -> bool:
299308 """Check if a processor is registered for a specific type.
300300-309309+301310 Args:
302311 type_name: The name of the type to check.
303303-312312+304313 Returns:
305314 True if a processor is registered for the specified type, False otherwise.
306315 """
307316 return type_name in self._processors and bool(self._processors[type_name])
308308-317317+309318 def clear(self) -> None:
310319 """Clear all registered processors."""
311320 self._processors.clear()
312312-321321+313322 def get_registered_types(self) -> set:
314323 """Get the set of all registered type names.
315315-324324+316325 Returns:
317326 A set of all registered type names.
318327 """
319328 return set(self._processors.keys())
320320-329329+321330 def to_hook_registry(self) -> TypeHookRegistry:
322331 """Convert this processor registry to a hook registry.
323323-332332+324333 This method creates a TypeHookRegistry that uses the decoders from
325334 this processor registry.
326326-335335+327336 Returns:
328337 A TypeHookRegistry with the same decoders as this processor registry.
329338 """
330339 hook_registry = TypeHookRegistry()
331331-340340+332341 for type_name, processors in self._processors.items():
333342 if processors and processors[0].decoder is not None:
334343 hook_registry.register_handler(type_name, processors[0].decoder)
335335-344344+336345 return hook_registry
337346338347···341350342351343352def register_type(
344344- type_name: str,
345345- priority: int = 0
353353+ type_name: str, priority: int = 0
346354) -> Callable[[Callable[[Dict[str, Any]], Any]], Callable[[Dict[str, Any]], Any]]:
347355 """Register a global type decoder function.
348348-356356+349357 This decorator registers a function as a global decoder for a specific type
350358 in the ATProto data model.
351351-359359+352360 Args:
353361 type_name: The name of the type to handle.
354362 priority: The priority of this processor (higher values = higher priority).
355355-363363+356364 Returns:
357365 A decorator function that registers the decorated function as a decoder.
358358-366366+359367 Example:
360368 >>> @register_type("app.bsky.feed.post", priority=10)
361369 ... def decode_post(data: Dict[str, Any]) -> Any:
···366374367375def get_global_processor_registry() -> TypeProcessorRegistry:
368376 """Get the global type processor registry.
369369-377377+370378 Returns:
371379 The global TypeProcessorRegistry instance.
372380 """
···374382375383376384def register_type_encoder(
377377- type_name: str,
378378- priority: int = 0
385385+ type_name: str, priority: int = 0
379386) -> Callable[[Callable[[Any], Dict[str, Any]]], Callable[[Any], Dict[str, Any]]]:
380387 """Register a global type encoder function.
381381-388388+382389 This decorator registers a function as a global encoder for a specific type
383390 in the ATProto data model.
384384-391391+385392 Args:
386393 type_name: The name of the type to handle.
387394 priority: The priority of this processor (higher values = higher priority).
388388-395395+389396 Returns:
390397 A decorator function that registers the decorated function as an encoder.
391391-398398+392399 Example:
393400 >>> @register_type_encoder("app.bsky.feed.post", priority=10)
394401 ... def encode_post(post: Post) -> Dict[str, Any]:
···398405399406400407def register_type_class(
401401- type_name: str,
402402- priority: int = 0
408408+ type_name: str, priority: int = 0
403409) -> Callable[[Type[T]], Type[T]]:
404410 """Register a class for both global encoding and decoding.
405405-411411+406412 This decorator registers a class for both encoding and decoding of a specific type
407413 in the ATProto data model.
408408-414414+409415 Args:
410416 type_name: The name of the type to handle.
411417 priority: The priority of this processor (higher values = higher priority).
412412-418418+413419 Returns:
414420 A decorator function that registers the decorated class.
415415-421421+416422 Example:
417423 >>> @register_type_class("app.bsky.feed.post", priority=10)
418424 ... class Post:
419425 ... def __init__(self, text: str, created_at: str) -> None:
420426 ... self.text = text
421427 ... self.created_at = created_at
422422- ...
428428+ ...
423429 ... @classmethod
424430 ... def from_json(cls, data: Dict[str, Any]) -> "Post":
425431 ... return cls(data["text"], data["createdAt"])
426426- ...
432432+ ...
427433 ... def to_json(self) -> Dict[str, Any]:
428434 ... return {"text": self.text, "createdAt": self.created_at}
429435 """
···432438433439def unregister_type(type_name: str, priority: Optional[int] = None) -> None:
434440 """Unregister global type processors.
435435-441441+436442 Args:
437443 type_name: The name of the type to unregister.
438444 priority: If specified, only unregister processors with this priority.
···442448443449def get_type_decoder(type_name: str) -> Optional[Callable[[Dict[str, Any]], Any]]:
444450 """Get the global decoder function for a specific type.
445445-451451+446452 Args:
447453 type_name: The name of the type to get the decoder for.
448448-454454+449455 Returns:
450456 The decoder function for the specified type, or None if no decoder
451457 is registered.
···455461456462def get_type_encoder(type_name: str) -> Optional[Callable[[Any], Dict[str, Any]]]:
457463 """Get the global encoder function for a specific type.
458458-464464+459465 Args:
460466 type_name: The name of the type to get the encoder for.
461461-467467+462468 Returns:
463469 The encoder function for the specified type, or None if no encoder
464470 is registered.
···468474469475def has_type_processor(type_name: str) -> bool:
470476 """Check if a global processor is registered for a specific type.
471471-477477+472478 Args:
473479 type_name: The name of the type to check.
474474-480480+475481 Returns:
476482 True if a processor is registered for the specified type, False otherwise.
477483 """
···485491486492def get_registered_types() -> set:
487493 """Get the set of all globally registered type names.
488488-494494+489495 Returns:
490496 A set of all registered type names.
491497 """
···494500495501def create_processor_registry() -> TypeProcessorRegistry:
496502 """Create a new type processor registry.
497497-503503+498504 This function creates a new, independent registry that can be used
499505 instead of the global registry.
500500-506506+501507 Returns:
502508 A new TypeProcessorRegistry instance.
503509 """
504504- return TypeProcessorRegistry()510510+ return TypeProcessorRegistry()
+42-35
src/atpasser/data/wrapper.py
···2929 sort_keys: bool = False,
3030 encoding: str = "utf-8",
3131 type_processor_registry: Optional[TypeProcessorRegistry] = None,
3232- **kwargs: Any
3232+ **kwargs: Any,
3333) -> None:
3434 """Serialize obj as a JSON formatted stream to fp.
3535-3535+3636 This function is similar to json.dump() but supports ATProto-specific
3737 data types, including bytes, CID links, and typed objects.
3838-3838+3939 Args:
4040 obj: The object to serialize.
4141 fp: A file-like object with a write() method.
···7070 """
7171 if cls is None:
7272 cls = JsonEncoder
7373-7373+7474 # Use the global type processor registry if none is provided
7575 if type_processor_registry is None:
7676 from .types import get_global_processor_registry
7777+7778 type_processor_registry = get_global_processor_registry()
7878-7979+7980 # Create an encoder instance with the specified encoding and type processor registry
8080- encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs)
8181-8181+ encoder = cls(
8282+ encoding=encoding, type_processor_registry=type_processor_registry, **kwargs
8383+ )
8484+8285 # Use the standard json.dump with our custom encoder
8386 json.dump(
8487 obj,
···9295 separators=separators,
9396 default=default,
9497 sort_keys=sort_keys,
9595- **kwargs
9898+ **kwargs,
9699 )
9710098101···110113 sort_keys: bool = False,
111114 encoding: str = "utf-8",
112115 type_processor_registry: Optional[TypeProcessorRegistry] = None,
113113- **kwargs: Any
116116+ **kwargs: Any,
114117) -> str:
115118 """Serialize obj to a JSON formatted string.
116116-119119+117120 This function is similar to json.dumps() but supports ATProto-specific
118121 data types, including bytes, CID links, and typed objects.
119119-122122+120123 Args:
121124 obj: The object to serialize.
122125 skipkeys: If True, dict keys that are not basic types (str, int, float,
···147150 encoding: The encoding to use for string serialization.
148151 type_processor_registry: Registry for type-specific processors.
149152 **kwargs: Additional keyword arguments to pass to the JSON encoder.
150150-153153+151154 Returns:
152155 A JSON formatted string.
153156 """
154157 if cls is None:
155158 cls = JsonEncoder
156156-159159+157160 # Create an encoder instance with the specified encoding and type processor registry
158158- encoder = cls(encoding=encoding, type_processor_registry=type_processor_registry, **kwargs)
159159-161161+ encoder = cls(
162162+ encoding=encoding, type_processor_registry=type_processor_registry, **kwargs
163163+ )
164164+160165 # Use the standard json.dumps with our custom encoder
161166 return json.dumps(
162167 obj,
···169174 separators=separators,
170175 default=default,
171176 sort_keys=sort_keys,
172172- **kwargs
177177+ **kwargs,
173178 )
174179175180···185190 type_hook_registry: Optional[TypeHookRegistry] = None,
186191 type_processor_registry: Optional[TypeProcessorRegistry] = None,
187192 encoding: str = "utf-8",
188188- **kwargs: Any
193193+ **kwargs: Any,
189194) -> Any:
190195 """Deserialize fp (a .read()-supporting text file or binary file containing
191196 a JSON document) to a Python object.
192192-197197+193198 This function is similar to json.load() but supports ATProto-specific
194199 data types, including bytes, CID links, and typed objects.
195195-200200+196201 Args:
197202 fp: A .read()-supporting text file or binary file containing a JSON document.
198203 cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
···220225 type_processor_registry: Registry for type-specific processors.
221226 encoding: The encoding to use for string deserialization.
222227 **kwargs: Additional keyword arguments to pass to the JSON decoder.
223223-228228+224229 Returns:
225230 A Python object.
226231 """
227232 if cls is None:
228233 cls = JsonDecoder
229229-234234+230235 # Use the global type hook registry if none is provided
231236 if type_hook_registry is None and type_processor_registry is None:
232237 from .hooks import get_global_registry
238238+233239 type_hook_registry = get_global_registry()
234240 elif type_processor_registry is not None:
235241 # Convert the type processor registry to a hook registry
236242 type_hook_registry = type_processor_registry.to_hook_registry()
237237-243243+238244 # Create a decoder instance with the specified parameters
239245 decoder = cls(
240246 object_hook=object_hook,
241247 type_hook_registry=type_hook_registry,
242248 encoding=encoding,
243243- **kwargs
249249+ **kwargs,
244250 )
245245-251251+246252 # Use the standard json.load with our custom decoder
247253 return json.load(
248254 fp,
···252258 parse_int=parse_int,
253259 parse_constant=parse_constant,
254260 object_pairs_hook=object_pairs_hook,
255255- **kwargs
261261+ **kwargs,
256262 )
257263258264···268274 type_hook_registry: Optional[TypeHookRegistry] = None,
269275 type_processor_registry: Optional[TypeProcessorRegistry] = None,
270276 encoding: str = "utf-8",
271271- **kwargs: Any
277277+ **kwargs: Any,
272278) -> Any:
273279 """Deserialize s (a str, bytes or bytearray instance containing a JSON document)
274280 to a Python object.
275275-281281+276282 This function is similar to json.loads() but supports ATProto-specific
277283 data types, including bytes, CID links, and typed objects.
278278-284284+279285 Args:
280286 s: A str, bytes or bytearray instance containing a JSON document.
281287 cls: A custom JSONDecoder subclass. If not specified, JsonDecoder is used.
···303309 type_processor_registry: Registry for type-specific processors.
304310 encoding: The encoding to use for string deserialization.
305311 **kwargs: Additional keyword arguments to pass to the JSON decoder.
306306-312312+307313 Returns:
308314 A Python object.
309315 """
310316 if cls is None:
311317 cls = JsonDecoder
312312-318318+313319 # Use the global type hook registry if none is provided
314320 if type_hook_registry is None and type_processor_registry is None:
315321 from .hooks import get_global_registry
322322+316323 type_hook_registry = get_global_registry()
317324 elif type_processor_registry is not None:
318325 # Convert the type processor registry to a hook registry
319326 type_hook_registry = type_processor_registry.to_hook_registry()
320320-327327+321328 # Create a decoder instance with the specified parameters
322329 decoder = cls(
323330 object_hook=object_hook,
324331 type_hook_registry=type_hook_registry,
325332 encoding=encoding,
326326- **kwargs
333333+ **kwargs,
327334 )
328328-335335+329336 # Use the standard json.loads with our custom decoder
330337 return json.loads(
331338 s,
···335342 parse_int=parse_int,
336343 parse_constant=parse_constant,
337344 object_pairs_hook=object_pairs_hook,
338338- **kwargs
339339- )345345+ **kwargs,
346346+ )
+1-1
tests/__init__.py
···11-"""Test package for atpasser."""11+"""Test package for atpasser."""
+1-1
tests/uri/__init__.py
···11-"""Test package for atpasser.uri module."""11+"""Test package for atpasser.uri module."""
+16-16
tests/uri/test_did.py
···1212 """Test creating a DID with a valid did:plc format."""
1313 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
1414 did = DID(did_str)
1515-1515+1616 assert str(did) == did_str
1717 assert did.uri == did_str
1818···2020 """Test creating a DID with a valid did:web format."""
2121 did_str = "did:web:blueskyweb.xyz"
2222 did = DID(did_str)
2323-2323+2424 assert str(did) == did_str
2525 assert did.uri == did_str
2626···2828 """Test creating a DID with various valid characters."""
2929 did_str = "did:method:val:two-with_underscores.and-dashes"
3030 did = DID(did_str)
3131-3131+3232 assert str(did) == did_str
3333 assert did.uri == did_str
34343535 def test_invalid_did_wrong_format(self):
3636 """Test that a DID with wrong format raises InvalidDIDError."""
3737 did_str = "not-a-did"
3838-3838+3939 with pytest.raises(InvalidDIDError, match="invalid format"):
4040 DID(did_str)
41414242 def test_invalid_did_uppercase_method(self):
4343 """Test that a DID with uppercase method raises InvalidDIDError."""
4444 did_str = "did:METHOD:val"
4545-4545+4646 with pytest.raises(InvalidDIDError, match="invalid format"):
4747 DID(did_str)
48484949 def test_invalid_did_method_with_numbers(self):
5050 """Test that a DID with method containing numbers raises InvalidDIDError."""
5151 did_str = "did:m123:val"
5252-5252+5353 with pytest.raises(InvalidDIDError, match="invalid format"):
5454 DID(did_str)
55555656 def test_invalid_did_empty_identifier(self):
5757 """Test that a DID with empty identifier raises InvalidDIDError."""
5858 did_str = "did:method:"
5959-5959+6060 with pytest.raises(InvalidDIDError, match="invalid format"):
6161 DID(did_str)
62626363 def test_invalid_did_ends_with_colon(self):
6464 """Test that a DID ending with colon raises InvalidDIDError."""
6565 did_str = "did:method:val:"
6666-6666+6767 with pytest.raises(InvalidDIDError, match="invalid format"):
6868 DID(did_str)
6969···7272 # Create a DID that exceeds the 2048 character limit
7373 long_identifier = "a" * 2040
7474 did_str = f"did:method:{long_identifier}"
7575-7575+7676 with pytest.raises(InvalidDIDError, match="exceeds maximum length"):
7777 DID(did_str)
7878···8181 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
8282 did1 = DID(did_str)
8383 did2 = DID(did_str)
8484-8484+8585 assert did1 == did2
8686 assert did1 != "not a did object"
8787···8989 """Test DID string representation."""
9090 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
9191 did = DID(did_str)
9292-9292+9393 assert str(did) == did_str
94949595 def test_did_fetch_plc_method(self):
9696 """Test fetching a DID document for did:plc method."""
9797 did_str = "did:plc:z72i7hdynmk6r22z27h6tvur"
9898 did = DID(did_str)
9999-9999+100100 # This test may fail if there's no internet connection or if the PLC directory is down
101101 try:
102102 document = did.fetch()
···110110 """Test fetching a DID document for did:web method."""
111111 did_str = "did:web:blueskyweb.xyz"
112112 did = DID(did_str)
113113-113113+114114 # This test may fail if there's no internet connection or if the web server is down
115115 try:
116116 document = did.fetch()
···124124 """Test that fetching a DID document with unsupported method raises InvalidDIDError."""
125125 did_str = "did:unsupported:method"
126126 did = DID(did_str)
127127-127127+128128 with pytest.raises(InvalidDIDError, match="unsupported DID method"):
129129 did.fetch()
130130···132132 """Test that fetching a DID document with empty domain raises InvalidDIDError."""
133133 did_str = "did:web:"
134134 did = DID(did_str)
135135-135135+136136 with pytest.raises(InvalidDIDError, match="invalid format"):
137137- did.fetch()137137+ did.fetch()
+22-22
tests/uri/test_handle.py
···1212 """Test creating a Handle with a valid simple format."""
1313 handle_str = "example.com"
1414 handle = Handle(handle_str)
1515-1515+1616 assert str(handle) == handle_str
1717 assert handle.handle == handle_str
1818···2020 """Test creating a Handle with a valid subdomain format."""
2121 handle_str = "subdomain.example.com"
2222 handle = Handle(handle_str)
2323-2323+2424 assert str(handle) == handle_str
2525 assert handle.handle == handle_str
2626···2828 """Test creating a Handle with a valid format containing hyphens."""
2929 handle_str = "my-example.com"
3030 handle = Handle(handle_str)
3131-3131+3232 assert str(handle) == handle_str
3333 assert handle.handle == handle_str
3434···3636 """Test creating a Handle with a valid format containing numbers."""
3737 handle_str = "example123.com"
3838 handle = Handle(handle_str)
3939-3939+4040 assert str(handle) == handle_str
4141 assert handle.handle == handle_str
4242···4444 """Test creating a Handle with a valid long domain name."""
4545 handle_str = "a" * 63 + "." + "b" * 63 + "." + "c" * 63 + ".com"
4646 handle = Handle(handle_str)
4747-4747+4848 assert str(handle) == handle_str
4949 assert handle.handle == handle_str
5050···5353 # Create a handle that exceeds the 253 character limit
5454 long_handle = "a" * 254
5555 handle_str = f"{long_handle}.com"
5656-5656+5757 with pytest.raises(InvalidHandleError, match="exceeds maximum length"):
5858 Handle(handle_str)
59596060 def test_invalid_handle_no_dot_separator(self):
6161 """Test that a Handle without a dot separator raises InvalidHandleError."""
6262 handle_str = "example"
6363-6363+6464 with pytest.raises(InvalidHandleError, match="invalid format"):
6565 Handle(handle_str)
66666767 def test_invalid_handle_starts_with_dot(self):
6868 """Test that a Handle starting with a dot raises InvalidHandleError."""
6969 handle_str = ".example.com"
7070-7070+7171 with pytest.raises(InvalidHandleError, match="invalid format"):
7272 Handle(handle_str)
73737474 def test_invalid_handle_ends_with_dot(self):
7575 """Test that a Handle ending with a dot raises InvalidHandleError."""
7676 handle_str = "example.com."
7777-7777+7878 with pytest.raises(InvalidHandleError, match="invalid format"):
7979 Handle(handle_str)
80808181 def test_invalid_handle_segment_too_long(self):
8282 """Test that a Handle with a segment that is too long raises InvalidHandleError."""
8383 handle_str = f"{'a' * 64}.com"
8484-8484+8585 with pytest.raises(InvalidHandleError, match="segment length error"):
8686 Handle(handle_str)
87878888 def test_invalid_handle_segment_empty(self):
8989 """Test that a Handle with an empty segment raises InvalidHandleError."""
9090 handle_str = "example..com"
9191-9191+9292 with pytest.raises(InvalidHandleError, match="segment length error"):
9393 Handle(handle_str)
94949595 def test_invalid_handle_invalid_characters(self):
9696 """Test that a Handle with invalid characters raises InvalidHandleError."""
9797 handle_str = "ex@mple.com"
9898-9898+9999 with pytest.raises(InvalidHandleError, match="contains invalid characters"):
100100 Handle(handle_str)
101101102102 def test_invalid_handle_segment_starts_with_hyphen(self):
103103 """Test that a Handle with a segment starting with a hyphen raises InvalidHandleError."""
104104 handle_str = "-example.com"
105105-105105+106106 with pytest.raises(InvalidHandleError, match="invalid format"):
107107 Handle(handle_str)
108108109109 def test_invalid_handle_segment_ends_with_hyphen(self):
110110 """Test that a Handle with a segment ending with a hyphen raises InvalidHandleError."""
111111 handle_str = "example-.com"
112112-112112+113113 with pytest.raises(InvalidHandleError, match="invalid format"):
114114 Handle(handle_str)
115115116116 def test_invalid_handle_tld_starts_with_digit(self):
117117 """Test that a Handle with a TLD starting with a digit raises InvalidHandleError."""
118118 handle_str = "example.1com"
119119-119119+120120 with pytest.raises(InvalidHandleError, match="invalid format"):
121121 Handle(handle_str)
122122···125125 handle_str = "example.com"
126126 handle1 = Handle(handle_str)
127127 handle2 = Handle(handle_str)
128128-128128+129129 assert handle1 == handle2
130130 assert handle1 != "not a handle object"
131131···133133 """Test Handle string representation."""
134134 handle_str = "example.com"
135135 handle = Handle(handle_str)
136136-136136+137137 assert str(handle) == handle_str
138138139139 def test_handle_case_insensitive_storage(self):
140140 """Test that Handle stores the handle in lowercase."""
141141 handle_str = "ExAmPlE.CoM"
142142 handle = Handle(handle_str)
143143-143143+144144 # The handle should be stored in lowercase
145145 assert handle.handle == "example.com"
146146 # The string representation should also return the lowercase form
···150150 """Test resolving a handle to DID using DNS method."""
151151 handle_str = "bsky.app"
152152 handle = Handle(handle_str)
153153-153153+154154 # This test may fail if there's no internet connection or if DNS resolution fails
155155 try:
156156 did = handle.toTID()
···164164 """Test resolving a handle to DID using HTTP method."""
165165 handle_str = "blueskyweb.xyz"
166166 handle = Handle(handle_str)
167167-167167+168168 # This test may fail if there's no internet connection or if HTTP resolution fails
169169 try:
170170 did = handle.toTID()
···178178 """Test resolving an unresolvable handle returns None."""
179179 handle_str = "nonexistent-domain-12345.com"
180180 handle = Handle(handle_str)
181181-181181+182182 # This should return None for a non-existent domain
183183 did = handle.toTID()
184184- assert did is None184184+ assert did is None
+39-33
tests/uri/test_nsid.py
···1212 """Test creating an NSID with a valid simple format."""
1313 nsid_str = "com.example.recordName"
1414 nsid = NSID(nsid_str)
1515-1515+1616 assert str(nsid) == nsid_str
1717 assert nsid.nsid == nsid_str
1818 assert nsid.domainAuthority == ["com", "example"]
···2424 """Test creating an NSID with a valid fragment."""
2525 nsid_str = "com.example.recordName#fragment"
2626 nsid = NSID(nsid_str)
2727-2727+2828 assert str(nsid) == nsid_str
2929 assert nsid.nsid == nsid_str
3030 assert nsid.domainAuthority == ["com", "example"]
···3636 """Test creating an NSID with multiple domain segments."""
3737 nsid_str = "net.users.bob.ping"
3838 nsid = NSID(nsid_str)
3939-3939+4040 assert str(nsid) == nsid_str
4141 assert nsid.nsid == nsid_str
4242 assert nsid.domainAuthority == ["net", "users", "bob"]
···4848 """Test creating an NSID with hyphens in domain segments."""
4949 nsid_str = "a-0.b-1.c.recordName"
5050 nsid = NSID(nsid_str)
5151-5151+5252 assert str(nsid) == nsid_str
5353 assert nsid.nsid == nsid_str
5454 assert nsid.domainAuthority == ["a-0", "b-1", "c"]
···6060 """Test creating an NSID with case-sensitive name."""
6161 nsid_str = "com.example.fooBar"
6262 nsid = NSID(nsid_str)
6363-6363+6464 assert str(nsid) == nsid_str
6565 assert nsid.nsid == nsid_str
6666 assert nsid.domainAuthority == ["com", "example"]
···7272 """Test creating an NSID with numbers in the name."""
7373 nsid_str = "com.example.record123"
7474 nsid = NSID(nsid_str)
7575-7575+7676 assert str(nsid) == nsid_str
7777 assert nsid.nsid == nsid_str
7878 assert nsid.domainAuthority == ["com", "example"]
···8383 def test_invalid_nsid_non_ascii_characters(self):
8484 """Test that an NSID with non-ASCII characters raises InvalidNSIDError."""
8585 nsid_str = "com.exa💩ple.thing"
8686-8686+8787 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
8888 NSID(nsid_str)
8989···9292 # Create an NSID that exceeds the 317 character limit
9393 long_segment = "a" * 100
9494 nsid_str = f"{long_segment}.{long_segment}.{long_segment}.recordName"
9595-9696- with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"):
9595+9696+ with pytest.raises(
9797+ InvalidNSIDError, match="domain authority length exceeds limit"
9898+ ):
9799 NSID(nsid_str)
9810099101 def test_invalid_nsid_starts_with_dot(self):
100102 """Test that an NSID starting with a dot raises InvalidNSIDError."""
101103 nsid_str = ".com.example.recordName"
102102-104104+103105 with pytest.raises(InvalidNSIDError, match="invalid format"):
104106 NSID(nsid_str)
105107106108 def test_invalid_nsid_ends_with_dot(self):
107109 """Test that an NSID ending with a dot raises InvalidNSIDError."""
108110 nsid_str = "com.example.recordName."
109109-111111+110112 with pytest.raises(InvalidNSIDError, match="invalid format"):
111113 NSID(nsid_str)
112114113115 def test_invalid_nsid_too_few_segments(self):
114116 """Test that an NSID with too few segments raises InvalidNSIDError."""
115117 nsid_str = "com.example"
116116-118118+117119 with pytest.raises(InvalidNSIDError, match="invalid format"):
118120 NSID(nsid_str)
119121···121123 """Test that an NSID with domain authority that is too long raises InvalidNSIDError."""
122124 # Create a domain authority that exceeds the 253 character limit
123125 long_segment = "a" * 63
124124- nsid_str = f"{long_segment}.{long_segment}.{long_segment}.{long_segment}.recordName"
125125-126126- with pytest.raises(InvalidNSIDError, match="domain authority length exceeds limit"):
126126+ nsid_str = (
127127+ f"{long_segment}.{long_segment}.{long_segment}.{long_segment}.recordName"
128128+ )
129129+130130+ with pytest.raises(
131131+ InvalidNSIDError, match="domain authority length exceeds limit"
132132+ ):
127133 NSID(nsid_str)
128134129135 def test_invalid_nsid_domain_segment_too_long(self):
130136 """Test that an NSID with a domain segment that is too long raises InvalidNSIDError."""
131137 nsid_str = f"{'a' * 64}.example.recordName"
132132-138138+133139 with pytest.raises(InvalidNSIDError, match="segment length error"):
134140 NSID(nsid_str)
135141136142 def test_invalid_nsid_domain_segment_empty(self):
137143 """Test that an NSID with an empty domain segment raises InvalidNSIDError."""
138144 nsid_str = "com..example.recordName"
139139-145145+140146 with pytest.raises(InvalidNSIDError, match="segment length error"):
141147 NSID(nsid_str)
142148143149 def test_invalid_nsid_domain_invalid_characters(self):
144150 """Test that an NSID with invalid characters in domain raises InvalidNSIDError."""
145151 nsid_str = "com.ex@mple.recordName"
146146-152152+147153 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
148154 NSID(nsid_str)
149155150156 def test_invalid_nsid_domain_segment_starts_with_hyphen(self):
151157 """Test that an NSID with a domain segment starting with a hyphen raises InvalidNSIDError."""
152158 nsid_str = "com.-example.recordName"
153153-159159+154160 with pytest.raises(InvalidNSIDError, match="invalid format"):
155161 NSID(nsid_str)
156162157163 def test_invalid_nsid_domain_segment_ends_with_hyphen(self):
158164 """Test that an NSID with a domain segment ending with a hyphen raises InvalidNSIDError."""
159165 nsid_str = "com.example-.recordName"
160160-166166+161167 with pytest.raises(InvalidNSIDError, match="invalid format"):
162168 NSID(nsid_str)
163169164170 def test_invalid_nsid_tld_starts_with_digit(self):
165171 """Test that an NSID with a TLD starting with a digit raises InvalidNSIDError."""
166172 nsid_str = "1com.example.recordName"
167167-173173+168174 with pytest.raises(InvalidNSIDError, match="invalid format"):
169175 NSID(nsid_str)
170176171177 def test_invalid_nsid_name_empty(self):
172178 """Test that an NSID with an empty name raises InvalidNSIDError."""
173179 nsid_str = "com.example."
174174-180180+175181 with pytest.raises(InvalidNSIDError, match="invalid format"):
176182 NSID(nsid_str)
177183178184 def test_invalid_nsid_name_too_long(self):
179185 """Test that an NSID with a name that is too long raises InvalidNSIDError."""
180186 nsid_str = f"com.example.{'a' * 64}"
181181-187187+182188 with pytest.raises(InvalidNSIDError, match="name length error"):
183189 NSID(nsid_str)
184190185191 def test_invalid_nsid_name_invalid_characters(self):
186192 """Test that an NSID with invalid characters in name raises InvalidNSIDError."""
187193 nsid_str = "com.example.record-name"
188188-194194+189195 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
190196 NSID(nsid_str)
191197192198 def test_invalid_nsid_name_starts_with_digit(self):
193199 """Test that an NSID with a name starting with a digit raises InvalidNSIDError."""
194200 nsid_str = "com.example.1record"
195195-201201+196202 with pytest.raises(InvalidNSIDError, match="invalid format"):
197203 NSID(nsid_str)
198204199205 def test_invalid_nsid_fragment_empty(self):
200206 """Test that an NSID with an empty fragment raises InvalidNSIDError."""
201207 nsid_str = "com.example.recordName#"
202202-208208+203209 with pytest.raises(InvalidNSIDError, match="fragment length error"):
204210 NSID(nsid_str)
205211206212 def test_invalid_nsid_fragment_too_long(self):
207213 """Test that an NSID with a fragment that is too long raises InvalidNSIDError."""
208214 nsid_str = f"com.example.recordName#{'a' * 64}"
209209-215215+210216 with pytest.raises(InvalidNSIDError, match="fragment length error"):
211217 NSID(nsid_str)
212218213219 def test_invalid_nsid_fragment_invalid_characters(self):
214220 """Test that an NSID with invalid characters in fragment raises InvalidNSIDError."""
215221 nsid_str = "com.example.recordName#fragment-with-hyphen"
216216-222222+217223 with pytest.raises(InvalidNSIDError, match="contains invalid characters"):
218224 NSID(nsid_str)
219225220226 def test_invalid_nsid_fragment_starts_with_digit(self):
221227 """Test that an NSID with a fragment starting with a digit raises InvalidNSIDError."""
222228 nsid_str = "com.example.recordName#1fragment"
223223-229229+224230 with pytest.raises(InvalidNSIDError, match="invalid format"):
225231 NSID(nsid_str)
226232···229235 nsid_str = "com.example.recordName"
230236 nsid1 = NSID(nsid_str)
231237 nsid2 = NSID(nsid_str)
232232-238238+233239 assert nsid1 == nsid2
234240 assert nsid1 != "not an nsid object"
235241···237243 """Test NSID string representation."""
238244 nsid_str = "com.example.recordName"
239245 nsid = NSID(nsid_str)
240240-246246+241247 assert str(nsid) == nsid_str
242248243249 def test_nsid_string_representation_with_fragment(self):
244250 """Test NSID string representation with fragment."""
245251 nsid_str = "com.example.recordName#fragment"
246252 nsid = NSID(nsid_str)
247247-248248- assert str(nsid) == nsid_str253253+254254+ assert str(nsid) == nsid_str
+27-17
tests/uri/test_restricted_uri.py
···10101111 def test_valid_restricted_uri_with_did_collection_and_rkey(self):
1212 """Test creating a RestrictedURI with a valid DID, collection, and rkey."""
1313- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
1313+ uri_str = (
1414+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
1515+ )
1416 uri = RestrictedURI(uri_str)
1515-1717+1618 assert str(uri) == uri_str
1719 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
1820 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···2628 """Test creating a RestrictedURI with a valid handle, collection, and rkey."""
2729 uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
2830 uri = RestrictedURI(uri_str)
2929-3131+3032 assert str(uri) == uri_str
3133 assert uri.authorityAsText == "bnewbold.bsky.team"
3234 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···3941 """Test creating a RestrictedURI with only a collection."""
4042 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
4143 uri = RestrictedURI(uri_str)
4242-4444+4345 assert str(uri) == uri_str
4446 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
4547 assert uri.path == ["app.bsky.feed.post"]
···5153 """Test creating a RestrictedURI with only an authority."""
5254 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
5355 uri = RestrictedURI(uri_str)
5454-5656+5557 assert str(uri) == uri_str
5658 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
5759 assert uri.path == []
···60626163 def test_invalid_restricted_uri_with_query(self):
6264 """Test that a RestrictedURI with query parameters raises InvalidRestrictedURIError."""
6363- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1"
6464-6565- with pytest.raises(InvalidRestrictedURIError, match="query parameters not supported"):
6565+ uri_str = (
6666+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1"
6767+ )
6868+6969+ with pytest.raises(
7070+ InvalidRestrictedURIError, match="query parameters not supported"
7171+ ):
6672 RestrictedURI(uri_str)
67736874 def test_invalid_restricted_uri_with_fragment(self):
6975 """Test that a RestrictedURI with a fragment raises InvalidRestrictedURIError."""
7076 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
7171-7777+7278 with pytest.raises(InvalidRestrictedURIError, match="fragments not supported"):
7379 RestrictedURI(uri_str)
74807581 def test_invalid_restricted_uri_with_invalid_authority(self):
7682 """Test that a RestrictedURI with invalid authority raises InvalidRestrictedURIError."""
7783 uri_str = "at://invalid_authority/app.bsky.feed.post/3jwdwj2ctlk26"
7878-8484+7985 with pytest.raises(InvalidRestrictedURIError, match="invalid authority"):
8086 RestrictedURI(uri_str)
81878288 def test_invalid_restricted_uri_too_many_path_segments(self):
8389 """Test that a RestrictedURI with too many path segments raises InvalidRestrictedURIError."""
8490 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26/extra"
8585-9191+8692 with pytest.raises(InvalidRestrictedURIError, match="too many path segments"):
8793 RestrictedURI(uri_str)
88948995 def test_invalid_restricted_uri_base_uri_validation_failure(self):
9096 """Test that a RestrictedURI with invalid base URI raises InvalidURIError."""
9197 uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
9292-9898+9399 with pytest.raises(InvalidURIError, match="invalid format"):
94100 RestrictedURI(uri_str)
9510196102 def test_restricted_uri_equality(self):
97103 """Test RestrictedURI equality comparison."""
9898- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
104104+ uri_str = (
105105+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
106106+ )
99107 uri1 = RestrictedURI(uri_str)
100108 uri2 = RestrictedURI(uri_str)
101101-109109+102110 assert uri1 == uri2
103111 assert uri1 != "not a uri object"
104112105113 def test_restricted_uri_string_representation(self):
106114 """Test RestrictedURI string representation."""
107107- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
115115+ uri_str = (
116116+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
117117+ )
108118 uri = RestrictedURI(uri_str)
109109-110110- assert str(uri) == uri_str119119+120120+ assert str(uri) == uri_str
+39-39
tests/uri/test_rkey.py
···1313 """Test creating an RKey with a valid simple format."""
1414 rkey_str = "3jui7kd54zh2y"
1515 rkey = RKey(rkey_str)
1616-1616+1717 assert str(rkey) == rkey_str
1818 assert rkey.recordKey == rkey_str
1919···2121 """Test creating an RKey with various valid characters."""
2222 rkey_str = "example.com"
2323 rkey = RKey(rkey_str)
2424-2424+2525 assert str(rkey) == rkey_str
2626 assert rkey.recordKey == rkey_str
2727···2929 """Test creating an RKey with valid special characters."""
3030 rkey_str = "~1.2-3_"
3131 rkey = RKey(rkey_str)
3232-3232+3333 assert str(rkey) == rkey_str
3434 assert rkey.recordKey == rkey_str
3535···3737 """Test creating an RKey with a colon."""
3838 rkey_str = "pre:fix"
3939 rkey = RKey(rkey_str)
4040-4040+4141 assert str(rkey) == rkey_str
4242 assert rkey.recordKey == rkey_str
4343···4545 """Test creating an RKey with just an underscore."""
4646 rkey_str = "_"
4747 rkey = RKey(rkey_str)
4848-4848+4949 assert str(rkey) == rkey_str
5050 assert rkey.recordKey == rkey_str
51515252 def test_invalid_rkey_empty(self):
5353 """Test that an empty RKey raises InvalidRKeyError."""
5454 rkey_str = ""
5555-5555+5656 with pytest.raises(InvalidRKeyError, match="record key is empty"):
5757 RKey(rkey_str)
5858···6060 """Test that an RKey that is too long raises InvalidRKeyError."""
6161 # Create an RKey that exceeds the 512 character limit
6262 rkey_str = "a" * 513
6363-6363+6464 with pytest.raises(InvalidRKeyError, match="exceeds maximum length"):
6565 RKey(rkey_str)
66666767 def test_invalid_rkey_reserved_double_dot(self):
6868 """Test that an RKey with '..' raises InvalidRKeyError."""
6969 rkey_str = ".."
7070-7070+7171 with pytest.raises(InvalidRKeyError, match="reserved value"):
7272 RKey(rkey_str)
73737474 def test_invalid_rkey_reserved_single_dot(self):
7575 """Test that an RKey with '.' raises InvalidRKeyError."""
7676 rkey_str = "."
7777-7777+7878 with pytest.raises(InvalidRKeyError, match="reserved value"):
7979 RKey(rkey_str)
80808181 def test_invalid_rkey_invalid_characters(self):
8282 """Test that an RKey with invalid characters raises InvalidRKeyError."""
8383 rkey_str = "alpha/beta"
8484-8484+8585 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
8686 RKey(rkey_str)
87878888 def test_invalid_rkey_hash_character(self):
8989 """Test that an RKey with a hash character raises InvalidRKeyError."""
9090 rkey_str = "#extra"
9191-9191+9292 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
9393 RKey(rkey_str)
94949595 def test_invalid_rkey_at_character(self):
9696 """Test that an RKey with an at character raises InvalidRKeyError."""
9797 rkey_str = "@handle"
9898-9898+9999 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
100100 RKey(rkey_str)
101101102102 def test_invalid_rkey_space(self):
103103 """Test that an RKey with a space raises InvalidRKeyError."""
104104 rkey_str = "any space"
105105-105105+106106 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
107107 RKey(rkey_str)
108108109109 def test_invalid_rkey_plus_character(self):
110110 """Test that an RKey with a plus character raises InvalidRKeyError."""
111111 rkey_str = "any+space"
112112-112112+113113 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
114114 RKey(rkey_str)
115115116116 def test_invalid_rkey_brackets(self):
117117 """Test that an RKey with brackets raises InvalidRKeyError."""
118118 rkey_str = "number[3]"
119119-119119+120120 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
121121 RKey(rkey_str)
122122123123 def test_invalid_rkey_parentheses(self):
124124 """Test that an RKey with parentheses raises InvalidRKeyError."""
125125 rkey_str = "number(3)"
126126-126126+127127 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
128128 RKey(rkey_str)
129129130130 def test_invalid_rkey_quotes(self):
131131 """Test that an RKey with quotes raises InvalidRKeyError."""
132132 rkey_str = '"quote"'
133133-133133+134134 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
135135 RKey(rkey_str)
136136137137 def test_invalid_rkey_base64_padding(self):
138138 """Test that an RKey with base64 padding raises InvalidRKeyError."""
139139 rkey_str = "dHJ1ZQ=="
140140-140140+141141 with pytest.raises(InvalidRKeyError, match="contains invalid characters"):
142142 RKey(rkey_str)
143143···146146 rkey_str = "3jui7kd54zh2y"
147147 rkey1 = RKey(rkey_str)
148148 rkey2 = RKey(rkey_str)
149149-149149+150150 assert rkey1 == rkey2
151151 assert rkey1 != "not an rkey object"
152152···154154 """Test RKey string representation."""
155155 rkey_str = "3jui7kd54zh2y"
156156 rkey = RKey(rkey_str)
157157-157157+158158 assert str(rkey) == rkey_str
159159160160···164164 def test_tid_creation_default(self):
165165 """Test creating a TID with default parameters."""
166166 tid = TID()
167167-167167+168168 assert isinstance(tid, TID)
169169 assert isinstance(tid, RKey)
170170 assert isinstance(tid.timestamp, datetime.datetime)
···176176 """Test creating a TID with a specific timestamp."""
177177 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
178178 tid = TID(time=timestamp)
179179-179179+180180 assert tid.timestamp == timestamp
181181 assert isinstance(tid.clockIdentifier, int)
182182 assert 0 <= tid.clockIdentifier < 1024
···185185 """Test creating a TID with a specific clock identifier."""
186186 clock_id = 42
187187 tid = TID(clockIdentifier=clock_id)
188188-188188+189189 assert tid.clockIdentifier == clock_id
190190 assert isinstance(tid.timestamp, datetime.datetime)
191191···194194 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
195195 clock_id = 42
196196 tid = TID(time=timestamp, clockIdentifier=clock_id)
197197-197197+198198 assert tid.timestamp == timestamp
199199 assert tid.clockIdentifier == clock_id
200200···203203 timestamp = datetime.datetime(2023, 1, 1, 12, 0, 0)
204204 clock_id = 42
205205 tid = TID(time=timestamp, clockIdentifier=clock_id)
206206-206206+207207 int_value = int(tid)
208208 expected_value = int(timestamp.timestamp() * 1000000) * 1024 + clock_id
209209-209209+210210 assert int_value == expected_value
211211212212 def test_tid_string_representation(self):
213213 """Test TID string representation."""
214214 tid = TID()
215215-215215+216216 str_value = str(tid)
217217 assert len(str_value) == 13
218218 assert all(c in "234567abcdefghijklmnopqrstuvwxyz" for c in str_value)
···223223 clock_id = 42
224224 tid1 = TID(time=timestamp, clockIdentifier=clock_id)
225225 tid2 = TID(time=timestamp, clockIdentifier=clock_id)
226226-226226+227227 assert tid1 == tid2
228228229229 def test_tid_equality_with_rkey(self):
···232232 clock_id = 42
233233 tid = TID(time=timestamp, clockIdentifier=clock_id)
234234 rkey = RKey(str(tid))
235235-235235+236236 assert tid == rkey
237237238238 def test_tid_inequality_with_different_object(self):
239239 """Test TID inequality comparison with a different object type."""
240240 tid = TID()
241241-241241+242242 assert tid != "not a tid object"
243243244244 def test_tid_inequality_with_different_timestamp(self):
···248248 clock_id = 42
249249 tid1 = TID(time=timestamp1, clockIdentifier=clock_id)
250250 tid2 = TID(time=timestamp2, clockIdentifier=clock_id)
251251-251251+252252 assert tid1 != tid2
253253254254 def test_tid_inequality_with_different_clock_id(self):
···258258 clock_id2 = 43
259259 tid1 = TID(time=timestamp, clockIdentifier=clock_id1)
260260 tid2 = TID(time=timestamp, clockIdentifier=clock_id2)
261261-261261+262262 assert tid1 != tid2
263263264264···268268 def test_import_tid_from_integer_default(self):
269269 """Test importing a TID from integer with default value."""
270270 tid = importTIDfromInteger()
271271-271271+272272 assert isinstance(tid, TID)
273273 assert isinstance(tid.timestamp, datetime.datetime)
274274 assert isinstance(tid.clockIdentifier, int)
···280280 clock_id = 42
281281 original_tid = TID(time=timestamp, clockIdentifier=clock_id)
282282 int_value = int(original_tid)
283283-283283+284284 imported_tid = importTIDfromInteger(int_value)
285285-285285+286286 assert imported_tid.timestamp == timestamp
287287 assert imported_tid.clockIdentifier == clock_id
288288289289 def test_import_tid_from_base32_default(self):
290290 """Test importing a TID from base32 with default value."""
291291 tid = importTIDfromBase32()
292292-292292+293293 assert isinstance(tid, TID)
294294 assert isinstance(tid.timestamp, datetime.datetime)
295295 assert isinstance(tid.clockIdentifier, int)
···299299 """Test importing a TID from base32 with a specific value."""
300300 original_tid = TID()
301301 str_value = str(original_tid)
302302-302302+303303 imported_tid = importTIDfromBase32(str_value)
304304-305305- assert int(imported_tid) == int(original_tid)304304+305305+ assert int(imported_tid) == int(original_tid)
+26-18
tests/uri/test_uri.py
···10101111 def test_valid_uri_with_did(self):
1212 """Test creating a URI with a valid DID."""
1313- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
1313+ uri_str = (
1414+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
1515+ )
1416 uri = URI(uri_str)
1515-1717+1618 assert str(uri) == uri_str
1719 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
1820 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···2628 """Test creating a URI with a valid handle."""
2729 uri_str = "at://bnewbold.bsky.team/app.bsky.feed.post/3jwdwj2ctlk26"
2830 uri = URI(uri_str)
2929-3131+3032 assert str(uri) == uri_str
3133 assert uri.authorityAsText == "bnewbold.bsky.team"
3234 assert uri.path == ["app.bsky.feed.post", "3jwdwj2ctlk26"]
···3638 """Test creating a URI with only a collection."""
3739 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
3840 uri = URI(uri_str)
3939-4141+4042 assert str(uri) == uri_str
4143 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
4244 assert uri.path == ["app.bsky.feed.post"]
···4648 """Test creating a URI with only an authority."""
4749 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur"
4850 uri = URI(uri_str)
4949-5151+5052 assert str(uri) == uri_str
5153 assert uri.authorityAsText == "did:plc:z72i7hdynmk6r22z27h6tvur"
5254 assert uri.path == []
···5658 """Test creating a URI with query parameters."""
5759 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post?param1=value1¶m2=value2"
5860 uri = URI(uri_str)
5959-6161+6062 assert uri.query == {"param1": ["value1"], "param2": ["value2"]}
6163 assert uri.queryAsText == "param1%3Dvalue1%26param2%3Dvalue2"
6264···6466 """Test creating a URI with a fragment."""
6567 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26#$.some.json.path"
6668 uri = URI(uri_str)
6767-6969+6870 assert uri.fragment is not None
6971 assert uri.fragmentAsText == "%24.some.json.path"
70727173 def test_invalid_uri_non_ascii_characters(self):
7274 """Test that non-ASCII characters in URI raise InvalidURIError."""
7375 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/💩"
7474-7676+7577 with pytest.raises(InvalidURIError, match="contains invalid characters"):
7678 URI(uri_str)
7779···8082 # Create a URI that exceeds the 8000 character limit
8183 long_path = "a" * 8000
8284 uri_str = f"at://did:plc:z72i7hdynmk6r22z27h6tvur/{long_path}"
8383-8585+8486 with pytest.raises(InvalidURIError, match="exceeds maximum length"):
8587 URI(uri_str)
86888789 def test_invalid_uri_wrong_scheme(self):
8890 """Test that a URI with wrong scheme raises InvalidURIError."""
8989- uri_str = "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
9090-9191+ uri_str = (
9292+ "https://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
9393+ )
9494+9195 with pytest.raises(InvalidURIError, match="invalid format"):
9296 URI(uri_str)
93979498 def test_invalid_uri_trailing_slash(self):
9599 """Test that a URI with trailing slash raises InvalidURIError."""
96100 uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/"
9797-101101+98102 with pytest.raises(InvalidURIError, match="cannot end with a slash"):
99103 URI(uri_str)
100104101105 def test_invalid_uri_with_userinfo(self):
102106 """Test that a URI with userinfo raises InvalidURIError."""
103107 uri_str = "at://user:pass@did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post"
104104-108108+105109 with pytest.raises(InvalidURIError, match="does not support user information"):
106110 URI(uri_str)
107111108112 def test_uri_equality(self):
109113 """Test URI equality comparison."""
110110- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
114114+ uri_str = (
115115+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
116116+ )
111117 uri1 = URI(uri_str)
112118 uri2 = URI(uri_str)
113113-119119+114120 assert uri1 == uri2
115121 assert uri1 != "not a uri object"
116122117123 def test_uri_string_representation(self):
118124 """Test URI string representation."""
119119- uri_str = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
125125+ uri_str = (
126126+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwdwj2ctlk26"
127127+ )
120128 uri = URI(uri_str)
121121-122122- assert str(uri) == uri_str129129+130130+ assert str(uri) == uri_str