ic-python-db 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,639 @@
1
+ """Property definitions for Entity classes."""
2
+
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Callable,
7
+ Generic,
8
+ Iterator,
9
+ List,
10
+ Optional,
11
+ Type,
12
+ TypeVar,
13
+ Union,
14
+ overload,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from .entity import Entity
19
+
20
+ # TypeVar for property value types
21
+ T = TypeVar("T")
22
+ # TypeVar for entity types in relations
23
+ E = TypeVar("E", bound="Entity")
24
+
25
+ # Prefix used for storing property values in entity __dict__
26
+ PROPERTY_STORAGE_PREFIX = "prop"
27
+
28
+
29
+ class Property(Generic[T]):
30
+ """Definition of an entity property.
31
+
32
+ A generic descriptor class that provides type-safe property access.
33
+ The type parameter T indicates the type of value this property holds.
34
+
35
+ Attributes:
36
+ name: Name of the property
37
+ type: Type of the property (e.g. str, int)
38
+ default: Default value if not set
39
+ validator: Optional function to validate values
40
+ """
41
+
42
+ name: str
43
+ type: Type[T]
44
+ default: Optional[T]
45
+ validator: Optional[Callable[[T], bool]]
46
+
47
+ def __init__(
48
+ self,
49
+ name: str = "",
50
+ type: Type[T] = type(None), # type: ignore[assignment]
51
+ default: Optional[T] = None,
52
+ validator: Optional[Callable[[T], bool]] = None,
53
+ ):
54
+ self.name = name
55
+ self.type = type
56
+ self.default = default
57
+ self.validator = validator
58
+
59
+ def __set_name__(self, owner: type, name: str) -> None:
60
+ """Set the property name when class is created."""
61
+ self.name = name
62
+
63
+ @overload
64
+ def __get__(self, obj: None, objtype: Optional[type]) -> "Property[T]": ...
65
+
66
+ @overload
67
+ def __get__(self, obj: object, objtype: Optional[type]) -> Optional[T]: ...
68
+
69
+ def __get__(
70
+ self, obj: object, objtype: Optional[type] = None
71
+ ) -> Union["Property[T]", Optional[T]]:
72
+ """Get the property value."""
73
+ if obj is None:
74
+ return self
75
+ return obj.__dict__.get(f"_{PROPERTY_STORAGE_PREFIX}_{self.name}", self.default)
76
+
77
+ def __set__(self, obj, value):
78
+ """Set the property value with type checking and validation."""
79
+ from .constants import ACTION_CREATE, ACTION_MODIFY
80
+ from .hooks import call_entity_hook
81
+
82
+ # Get old value and determine action
83
+ old_value = obj.__dict__.get(
84
+ f"_{PROPERTY_STORAGE_PREFIX}_{self.name}", self.default
85
+ )
86
+ action = (
87
+ ACTION_CREATE
88
+ if not hasattr(obj, "_loaded") or not obj._loaded
89
+ else ACTION_MODIFY
90
+ )
91
+
92
+ # Call hook before setting
93
+ allow, modified_value = call_entity_hook(
94
+ obj, self.name, old_value, value, action
95
+ )
96
+
97
+ if not allow:
98
+ raise ValueError(f"Hook rejected change to {self.name}")
99
+
100
+ value = modified_value
101
+
102
+ if value is not None:
103
+ if not isinstance(value, self.type):
104
+ if isinstance(value, str) and self.type in (int, float, bool):
105
+ try:
106
+ if self.type == bool:
107
+ value = value.lower() in ("true", "1", "yes", "on")
108
+ else:
109
+ value = self.type(value)
110
+ except (ValueError, TypeError):
111
+ raise TypeError(
112
+ f"{self.name} must be of type {self.type.__name__}"
113
+ )
114
+ else:
115
+ raise TypeError(f"{self.name} must be of type {self.type.__name__}")
116
+
117
+ if self.validator and not self.validator(value):
118
+ raise ValueError(f"Invalid value for {self.name}: {value}")
119
+
120
+ obj.__dict__[f"_{PROPERTY_STORAGE_PREFIX}_{self.name}"] = value
121
+ obj._save()
122
+
123
+
124
+ class String(Property[str]):
125
+ """String property with optional length validation."""
126
+
127
+ def __init__(
128
+ self,
129
+ min_length: Optional[int] = None,
130
+ max_length: Optional[int] = None,
131
+ default: Optional[str] = None,
132
+ ):
133
+ def validator(value: str) -> bool:
134
+ if min_length is not None and len(value) < min_length:
135
+ return False
136
+ if max_length is not None and len(value) > max_length:
137
+ return False
138
+ return True
139
+
140
+ super().__init__(name="", type=str, default=default, validator=validator)
141
+
142
+
143
+ class Integer(Property[int]):
144
+ """Integer property with optional range validation."""
145
+
146
+ def __init__(
147
+ self,
148
+ min_value: Optional[int] = None,
149
+ max_value: Optional[int] = None,
150
+ default: Optional[int] = None,
151
+ ):
152
+ def validator(value: int) -> bool:
153
+ if min_value is not None and value < min_value:
154
+ return False
155
+ if max_value is not None and value > max_value:
156
+ return False
157
+ return True
158
+
159
+ super().__init__(name="", type=int, default=default, validator=validator)
160
+
161
+
162
+ class Float(Property[float]):
163
+ """Float property with optional range validation."""
164
+
165
+ def __init__(
166
+ self,
167
+ min_value: Optional[float] = None,
168
+ max_value: Optional[float] = None,
169
+ default: Optional[float] = None,
170
+ ):
171
+ def validator(value: float) -> bool:
172
+ if min_value is not None and value < min_value:
173
+ return False
174
+ if max_value is not None and value > max_value:
175
+ return False
176
+ return True
177
+
178
+ super().__init__(name="", type=float, default=default, validator=validator)
179
+
180
+
181
+ class Boolean(Property[bool]):
182
+ """Boolean property."""
183
+
184
+ def __init__(self, default: Optional[bool] = None):
185
+ super().__init__(name="", type=bool, default=default)
186
+
187
+
188
+ class Relation(Generic[E]):
189
+ """Base property for defining and accessing relations.
190
+
191
+ This is the base class for all relation properties. It provides common functionality
192
+ for managing relationships between entities.
193
+
194
+ For one-to-one relationships (default), this property returns a single entity and
195
+ enforces single cardinality. For one-to-many or many-to-many relationships, it
196
+ returns a list of entities.
197
+
198
+ The type parameter E indicates the type of related entity.
199
+ """
200
+
201
+ name: Optional[str]
202
+ entity_types: Union[str, List[str]]
203
+ reverse_name: Optional[str]
204
+ many: bool
205
+
206
+ def __init__(
207
+ self,
208
+ entity_types: Union[str, List[str]],
209
+ reverse_name: Optional[str] = None,
210
+ many: bool = False,
211
+ ):
212
+ """Initialize relation property.
213
+
214
+ Args:
215
+ entity_types: Type names of related entities
216
+ reverse_name: Optional name for reverse relation
217
+ many: Whether this property can hold multiple entities (default: False)
218
+ """
219
+ self.entity_types = entity_types
220
+ self.name = None
221
+ self.reverse_name = reverse_name
222
+ self.many = many
223
+
224
+ def __set_name__(self, owner: type, name: str) -> None:
225
+ """Set the property name when class is created."""
226
+ self.name = name
227
+ if self.reverse_name is None:
228
+ self.reverse_name = name
229
+
230
+ @overload
231
+ def __get__(self, obj: None, objtype: Optional[type]) -> "Relation[E]": ...
232
+
233
+ @overload
234
+ def __get__(
235
+ self, obj: object, objtype: Optional[type]
236
+ ) -> Union[Optional[E], List[E]]: ...
237
+
238
+ def __get__(
239
+ self, obj: object, objtype: Optional[type] = None
240
+ ) -> Union["Relation[E]", Optional[E], List[E]]:
241
+ """Get related entities."""
242
+ if obj is None:
243
+ return self
244
+ relations = obj.get_relations(self.name)
245
+ if not self.many:
246
+ # For single relationships, return the first entity or None
247
+ return relations[0] if relations else None
248
+ return relations
249
+
250
+ def __set__(self, obj, value):
251
+ """Set related entities.
252
+
253
+ Args:
254
+ value: For many=False: a single Entity instance
255
+ For many=True: a list/tuple of Entity instances
256
+ """
257
+ if self.many:
258
+ if not isinstance(value, (list, tuple)):
259
+ raise TypeError(f"{self.name} requires a list or tuple of entities")
260
+ values_list = value
261
+ else:
262
+ values_list = [value]
263
+
264
+ # Get existing and new relations as sets
265
+ existing = set(obj.get_relations(self.name))
266
+ new = set(values_list)
267
+
268
+ # For one-to-many, check if entities have existing relations
269
+ if isinstance(self, OneToMany):
270
+ for entity in new:
271
+ existing_relations = entity.get_relations(self.reverse_name)
272
+ if existing_relations:
273
+ old_relation = existing_relations[0]
274
+ if old_relation != obj:
275
+ # Remove the entity from the old relation's list
276
+ old_relation._relations[self.name].remove(entity)
277
+ # Remove the old relation from the entity's list
278
+ entity._relations[self.reverse_name].remove(old_relation)
279
+
280
+ # Remove relations that are not in new set
281
+ to_remove = existing - new
282
+ for entity in to_remove:
283
+ obj.remove_relation(self.name, self.reverse_name, entity)
284
+
285
+ # Add relations that are not in existing set
286
+ to_add = new - existing
287
+ for entity in to_add:
288
+ obj.add_relation(self.name, self.reverse_name, entity)
289
+
290
+ def validate_entity(self, entity: Any) -> bool:
291
+ """Validate that an entity is of the correct type.
292
+
293
+ Handles both namespaced (e.g., "app::User") and non-namespaced (e.g., "User") types.
294
+ """
295
+ from .entity import Entity
296
+
297
+ if entity is None:
298
+ return True
299
+ if not isinstance(entity, Entity):
300
+ raise TypeError(f"{self.name} must be set to Entity instances")
301
+
302
+ # Convert entity_types to list for uniform handling
303
+ allowed_types = (
304
+ [self.entity_types]
305
+ if isinstance(self.entity_types, str)
306
+ else self.entity_types
307
+ )
308
+
309
+ # Check if entity type matches any allowed type
310
+ # This handles both exact matches and namespace variations
311
+ if entity._type not in allowed_types:
312
+ # Also check if the class name matches (for backward compatibility)
313
+ entity_class_name = (
314
+ entity._type.split("::")[-1] if "::" in entity._type else entity._type
315
+ )
316
+ type_matches = any(
317
+ entity_class_name == (t.split("::")[-1] if "::" in t else t)
318
+ for t in allowed_types
319
+ )
320
+ if not type_matches:
321
+ raise TypeError(
322
+ f"{self.name} must be set an Entity instance of any of the following types: {self.entity_types}, "
323
+ f"but got type '{entity._type}'"
324
+ )
325
+
326
+ return True
327
+
328
+ def resolve_entity(self, obj: Any, value: Any) -> Optional[E]:
329
+ """Resolve a value to an Entity instance.
330
+
331
+ Args:
332
+ obj: The entity object that owns this relation
333
+ value: Can be an Entity instance, string ID, or string name/alias
334
+
335
+ Returns:
336
+ Entity instance or None
337
+ """
338
+ from .entity import Entity
339
+
340
+ if value is None:
341
+ return None
342
+
343
+ if isinstance(value, Entity):
344
+ return value # type: ignore[return-value]
345
+
346
+ if isinstance(value, (str, int)):
347
+ # Try to find entity by ID or name (alias) using each allowed entity type
348
+ entity_types = (
349
+ [self.entity_types]
350
+ if isinstance(self.entity_types, str)
351
+ else self.entity_types
352
+ )
353
+ for entity_type_name in entity_types:
354
+ # Get the entity class from the database registry
355
+ entity_class = obj.db()._entity_types.get(entity_type_name)
356
+
357
+ if entity_class:
358
+ found_entity = entity_class[value]
359
+ if found_entity:
360
+ return found_entity
361
+
362
+ raise ValueError(
363
+ f"No entity of types {self.entity_types} found with ID or name '{value}'"
364
+ )
365
+
366
+ raise TypeError(
367
+ f"{self.name} must be set to an Entity instance, string ID, or string name"
368
+ )
369
+
370
+
371
+ class RelationList(Generic[E]):
372
+ """Helper class for managing lists of related entities."""
373
+
374
+ def __init__(self, obj: Any, prop: "Relation[E]"):
375
+ self.obj = obj
376
+ self.prop = prop
377
+
378
+ def add(self, entity: Union[E, str, int]) -> None:
379
+ """Add a new relation."""
380
+ # Resolve entity (supports string ID/name)
381
+ resolved = self.prop.resolve_entity(self.obj, entity)
382
+
383
+ # Validate entity type using the base validate_entity method
384
+ self.prop.validate_entity(resolved)
385
+
386
+ # For one-to-many, check if entity already has a relation
387
+ if isinstance(self.prop, OneToMany):
388
+ existing_relations = resolved.get_relations(self.prop.reverse_name)
389
+ if existing_relations:
390
+ # Remove existing relation since it's one-to-many
391
+ old_relation = existing_relations[0]
392
+ # Remove the entity from the old relation's list
393
+ if old_relation != self.obj:
394
+ old_relation._relations[self.prop.name].remove(resolved)
395
+ # Remove the old relation from the entity's list
396
+ resolved._relations[self.prop.reverse_name].remove(old_relation)
397
+
398
+ self.obj.add_relation(self.prop.name, self.prop.reverse_name, resolved)
399
+
400
+ def remove(self, entity: Union[E, str, int]) -> None:
401
+ """Remove a relation."""
402
+ self.obj.remove_relation(self.prop.name, self.prop.reverse_name, entity)
403
+
404
+ def __iter__(self) -> "Iterator[E]":
405
+ return iter(self.obj.get_relations(self.prop.name))
406
+
407
+ def __len__(self) -> int:
408
+ return len(self.obj.get_relations(self.prop.name))
409
+
410
+
411
+ class OneToOne(Relation[E]):
412
+ """Property for defining one-to-one relationships.
413
+
414
+ This property type represents a one-to-one relationship where each entity
415
+ can be related to exactly one entity on the other side.
416
+
417
+ Example:
418
+ class Person(Entity):
419
+ profile = OneToOne('Profile', 'person')
420
+
421
+ class Profile(Entity):
422
+ person = OneToOne('Person', 'profile')
423
+ """
424
+
425
+ def __init__(
426
+ self,
427
+ entity_types: Union[str, List[str]],
428
+ reverse_name: Optional[str] = None,
429
+ ):
430
+ super().__init__(entity_types, reverse_name, many=False)
431
+
432
+ def __set__(self, obj, value):
433
+ """Set the related entity with one-to-one constraints."""
434
+ if value is not None:
435
+ # Check if trying to set multiple values
436
+ if isinstance(value, (list, tuple)):
437
+ raise ValueError(
438
+ f"{self.name} cannot be set to multiple values (one-to-one relationship)"
439
+ )
440
+
441
+ # Validate entity type
442
+ value = self.resolve_entity(obj, value)
443
+
444
+ # Check that the reverse property is OneToOne
445
+ reverse_prop = value.__class__.__dict__.get(self.reverse_name)
446
+ if not isinstance(reverse_prop, OneToOne):
447
+ raise ValueError(
448
+ f"Reverse property '{self.reverse_name}' must be OneToOne"
449
+ )
450
+
451
+ # Get current value if any
452
+ current = self.__get__(obj)
453
+ if current is not None:
454
+ # Remove existing relation
455
+ obj.remove_relation(self.name, self.reverse_name, current)
456
+
457
+ # Check if value is already related to another entity and remove that relation
458
+ existing = value.get_relations(self.reverse_name)
459
+ if existing:
460
+ existing_entity = existing[0]
461
+ # Remove the existing relation from both sides
462
+ existing_entity.remove_relation(self.name, self.reverse_name, value)
463
+ value.remove_relation(self.reverse_name, self.name, existing_entity)
464
+
465
+ # Set the new relation
466
+ super().__set__(obj, value)
467
+
468
+
469
+ class OneToMany(Relation[E]):
470
+ """Property for defining one-to-many relationships.
471
+
472
+ This property type represents a one-to-many relationship where the 'one' side
473
+ owns multiple instances of the 'many' side, but each 'many' instance belongs to
474
+ only one owner.
475
+
476
+ Example:
477
+ class Department(Entity):
478
+ employees = OneToMany('Employee', 'department')
479
+
480
+ class Employee(Entity):
481
+ department = ManyToOne('Department', 'employees')
482
+ """
483
+
484
+ def __init__(self, entity_types: Union[str, List[str]], reverse_name: str):
485
+ super().__init__(entity_types, reverse_name, many=True)
486
+
487
+ def __set__(self, obj, values):
488
+ """Set related entities with one-to-many constraints."""
489
+ if not isinstance(values, (list, tuple)):
490
+ raise TypeError(f"{self.name} must be set to a list of entities")
491
+
492
+ resolved_values = []
493
+ for value in values:
494
+ # Resolve value to Entity instance
495
+ resolved_value = self.resolve_entity(obj, value)
496
+
497
+ # Validate entity type using the base validate_entity method
498
+ self.validate_entity(resolved_value)
499
+ resolved_values.append(resolved_value)
500
+
501
+ # Check that the reverse property is ManyToOne
502
+ reverse_prop = resolved_value.__class__.__dict__.get(self.reverse_name)
503
+ if not isinstance(reverse_prop, ManyToOne):
504
+ raise ValueError(
505
+ f"Reverse property '{self.reverse_name}' must be ManyToOne"
506
+ )
507
+
508
+ # Replace original values with resolved entities
509
+ super().__set__(obj, resolved_values)
510
+
511
+ @overload
512
+ def __get__(self, obj: None, objtype: Optional[type]) -> "OneToMany[E]": ...
513
+
514
+ @overload
515
+ def __get__(self, obj: object, objtype: Optional[type]) -> "RelationList[E]": ...
516
+
517
+ def __get__(
518
+ self, obj: object, objtype: Optional[type] = None
519
+ ) -> Union["OneToMany[E]", "RelationList[E]"]:
520
+ """Get related entities as a RelationList."""
521
+ if obj is None:
522
+ return self
523
+ return RelationList(obj, self)
524
+
525
+
526
+ class ManyToOne(Relation[E]):
527
+ """Property for defining many-to-one relationships.
528
+
529
+ This property type represents a many-to-one relationship where multiple entities
530
+ can belong to a single owner entity.
531
+
532
+ Example:
533
+ class Employee(Entity):
534
+ department = ManyToOne('Department', 'employees')
535
+
536
+ class Department(Entity):
537
+ employees = OneToMany('Employee', 'department')
538
+ """
539
+
540
+ def __init__(
541
+ self, entity_types: Union[str, List[str]], reverse_name: Optional[str] = None
542
+ ):
543
+ super().__init__(entity_types, reverse_name, many=False)
544
+
545
+ def __set__(self, obj, value):
546
+ """Set the related entity with many-to-one constraints."""
547
+ if value is not None:
548
+ # Check if trying to set multiple values
549
+ if isinstance(value, (list, tuple)):
550
+ raise ValueError(
551
+ f"{self.name} cannot be set to multiple values (many-to-one relationship)"
552
+ )
553
+
554
+ # Resolve value to Entity instance
555
+ value = self.resolve_entity(obj, value)
556
+
557
+ # Validate entity type using the base validate_entity method
558
+ self.validate_entity(value)
559
+
560
+ # Check that the reverse property is OneToMany
561
+ reverse_prop = value.__class__.__dict__.get(self.reverse_name)
562
+ if not reverse_prop:
563
+ raise ValueError(
564
+ f"Reverse property '{self.reverse_name}' not found in {value.__class__.__name__} entity"
565
+ )
566
+
567
+ if not isinstance(reverse_prop, OneToMany):
568
+ raise ValueError(
569
+ f"Reverse property '{self.reverse_name}' must be OneToMany and it is '{reverse_prop.__class__.__name__}'"
570
+ )
571
+
572
+ super().__set__(obj, value)
573
+
574
+
575
+ class ManyToMany(Relation[E]):
576
+ """Property for defining many-to-many relationships.
577
+
578
+ This property type represents a many-to-many relationship where entities
579
+ on both sides can be related to multiple entities on the other side.
580
+
581
+ Example:
582
+ class Student(Entity):
583
+ courses = ManyToMany('Course', 'students')
584
+
585
+ class Course(Entity):
586
+ students = ManyToMany('Student', 'courses')
587
+ """
588
+
589
+ def __init__(self, entity_types: Union[str, List[str]], reverse_name: str):
590
+ super().__init__(entity_types, reverse_name, many=True)
591
+
592
+ def __set__(self, obj, values):
593
+ """Set related entities with many-to-many constraints."""
594
+ from .entity import Entity
595
+
596
+ # Convert single entity to list for convenience
597
+ if isinstance(values, Entity):
598
+ values = [values]
599
+ elif isinstance(values, (str, int)):
600
+ # Handle single string ID/name
601
+ values = [values]
602
+ elif values is not None and not isinstance(values, (list, tuple)):
603
+ raise TypeError(f"{self.name} must be set to an entity or list of entities")
604
+
605
+ if values is not None:
606
+ resolved_values = []
607
+ for value in values:
608
+ # Resolve value to Entity instance
609
+ resolved_value = self.resolve_entity(obj, value)
610
+
611
+ # Validate entity type using the base validate_entity method
612
+ self.validate_entity(resolved_value)
613
+ resolved_values.append(resolved_value)
614
+
615
+ # Check that the reverse property is ManyToMany
616
+ reverse_prop = resolved_value.__class__.__dict__.get(self.reverse_name)
617
+ if not isinstance(reverse_prop, ManyToMany):
618
+ raise ValueError(
619
+ f"Reverse property '{self.reverse_name}' must be ManyToMany"
620
+ )
621
+
622
+ # Replace original values with resolved entities
623
+ values = resolved_values
624
+
625
+ super().__set__(obj, values)
626
+
627
+ @overload
628
+ def __get__(self, obj: None, objtype: Optional[type]) -> "ManyToMany[E]": ...
629
+
630
+ @overload
631
+ def __get__(self, obj: object, objtype: Optional[type]) -> "RelationList[E]": ...
632
+
633
+ def __get__(
634
+ self, obj: object, objtype: Optional[type] = None
635
+ ) -> Union["ManyToMany[E]", "RelationList[E]"]:
636
+ """Get related entities as a RelationList."""
637
+ if obj is None:
638
+ return self
639
+ return RelationList(obj, self)
ic_python_db/py.typed ADDED
File without changes
@@ -0,0 +1,69 @@
1
+ """
2
+ Storage backends for IC Python DB
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Iterator, Optional, Tuple
7
+
8
+
9
+ class Storage(ABC):
10
+ """Abstract base class for storage backends"""
11
+
12
+ @abstractmethod
13
+ def insert(self, key: str, value: str) -> None:
14
+ """Insert a key-value pair into storage"""
15
+ raise NotImplementedError
16
+
17
+ @abstractmethod
18
+ def get(self, key: str) -> Optional[str]:
19
+ """Retrieve a value by key"""
20
+ raise NotImplementedError
21
+
22
+ @abstractmethod
23
+ def remove(self, key: str) -> None:
24
+ """Remove a key-value pair from storage"""
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def items(self) -> Iterator[Tuple[str, str]]:
29
+ """Return all items in storage"""
30
+ raise NotImplementedError
31
+
32
+ @abstractmethod
33
+ def __contains__(self, key: str) -> bool:
34
+ """Check if key exists in storage"""
35
+ raise NotImplementedError
36
+
37
+ @abstractmethod
38
+ def keys(self) -> Iterator[str]:
39
+ """Return all keys in storage"""
40
+ raise NotImplementedError
41
+
42
+
43
+ class MemoryStorage(Storage):
44
+ """In-memory storage implementation using Python dictionary"""
45
+
46
+ def __init__(self):
47
+ self._data: Dict[str, str] = {}
48
+
49
+ def insert(self, key: str, value: str) -> None:
50
+ self._data[key] = value
51
+
52
+ def get(self, key: str) -> Optional[str]:
53
+ return self._data.get(key)
54
+
55
+ def remove(self, key: str) -> None:
56
+ if key in self._data:
57
+ del self._data[key]
58
+ else:
59
+ raise KeyError(f"Key '{key}' not found in storage")
60
+
61
+ def items(self) -> Iterator[Tuple[str, str]]:
62
+ return iter(self._data.items())
63
+
64
+ def __contains__(self, key: str) -> bool:
65
+ return key in self._data
66
+
67
+ def keys(self) -> Iterator[str]:
68
+ """Return all keys in storage"""
69
+ return iter(self._data.keys())