formatted

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