persistence-kit 2.0.0__tar.gz → 3.0.0__tar.gz

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.
Files changed (46) hide show
  1. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/PKG-INFO +2 -1
  2. persistence_kit-3.0.0/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +111 -0
  3. persistence_kit-3.0.0/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +267 -0
  4. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/factory/repository_factory.py +17 -0
  5. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/settings/constants.py +1 -0
  6. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/settings/repo_settings.py +3 -0
  7. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/pyproject.toml +2 -1
  8. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/LICENSE +0 -0
  9. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/README.md +0 -0
  10. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/__init__.py +0 -0
  11. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/api/__init__.py +0 -0
  12. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/api/common.py +0 -0
  13. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/api/error_handlers.py +0 -0
  14. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/api/exceptions.py +0 -0
  15. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/api/route_loader.py +0 -0
  16. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/bootstrap/__init__.py +0 -0
  17. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/bootstrap/configuration.py +0 -0
  18. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/bootstrap/seeders.py +0 -0
  19. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/bootstrap/startup.py +0 -0
  20. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/contracts/__init__.py +0 -0
  21. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/contracts/repository.py +0 -0
  22. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/contracts/view_repository.py +0 -0
  23. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/py.typed +0 -0
  24. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/__init__.py +0 -0
  25. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/filter_ops.py +0 -0
  26. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/memory_repo/__init__.py +0 -0
  27. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
  28. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
  29. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
  30. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/mongo_repo/mongo_repo.py +0 -0
  31. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
  32. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
  33. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
  34. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +0 -0
  35. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +0 -0
  36. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
  37. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/__init__.py +0 -0
  38. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/factory/__init__.py +0 -0
  39. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/registry/__init__.py +0 -0
  40. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
  41. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/view/__init__.py +0 -0
  42. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
  43. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/settings/__init__.py +0 -0
  44. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/settings/parsers.py +0 -0
  45. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/utils/__init__.py +0 -0
  46. {persistence_kit-2.0.0 → persistence_kit-3.0.0}/persistence_kit/utils/upsert.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: persistence-kit
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: Reusable persistence and repository toolkit
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Database
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Dist: asyncpg (>=0.30.0,<0.31.0)
22
+ Requires-Dist: boto3 (>=1.35.0,<2.0.0)
22
23
  Requires-Dist: fastapi (>=0.115.0,<0.116.0)
23
24
  Requires-Dist: motor (>=3.7.1,<4.0.0)
24
25
  Requires-Dist: pydantic-settings (>=2.3.0,<3.0.0)
@@ -0,0 +1,111 @@
1
+ from dataclasses import asdict, is_dataclass, fields
2
+ from typing import Any, Mapping, Type, TypeVar
3
+ from uuid import UUID
4
+ from datetime import datetime, date
5
+ from decimal import Decimal
6
+ from enum import Enum
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ def _serialize_value(value: Any) -> Any:
12
+ if value is None:
13
+ return None
14
+ if isinstance(value, UUID):
15
+ return str(value)
16
+ if isinstance(value, datetime):
17
+ return value.isoformat()
18
+ if isinstance(value, date):
19
+ return value.isoformat()
20
+ if isinstance(value, Enum):
21
+ return value.value
22
+ if isinstance(value, float):
23
+ return Decimal(str(value))
24
+ if isinstance(value, dict):
25
+ return {k: _serialize_value(v) for k, v in value.items()}
26
+ if isinstance(value, (list, tuple)):
27
+ return [_serialize_value(v) for v in value]
28
+ return value
29
+
30
+
31
+ def _deserialize_value(value: Any, target_type: type | None = None) -> Any:
32
+ if value is None:
33
+ return None
34
+ if isinstance(value, Decimal):
35
+ if value == int(value):
36
+ return int(value)
37
+ return float(value)
38
+ if isinstance(value, dict) and target_type is None:
39
+ return {k: _deserialize_value(v) for k, v in value.items()}
40
+ if isinstance(value, list):
41
+ return [_deserialize_value(v) for v in value]
42
+ return value
43
+
44
+
45
+ class DynamoMapper:
46
+ def __init__(
47
+ self,
48
+ entity_type: Type[T],
49
+ table_name: str,
50
+ unique_fields: dict[str, str] | None = None,
51
+ ) -> None:
52
+ if not is_dataclass(entity_type):
53
+ raise TypeError("Entity type must be a dataclass")
54
+ self._entity_type = entity_type
55
+ self._table_name = table_name
56
+ self._unique_fields = dict(unique_fields or {})
57
+ self._field_names = {f.name for f in fields(entity_type)}
58
+
59
+ def table_name(self) -> str:
60
+ return self._table_name
61
+
62
+ def id_of(self, entity: T) -> Any:
63
+ return getattr(entity, "id")
64
+
65
+ def to_item(self, entity: T) -> dict[str, Any]:
66
+ d = asdict(entity)
67
+ result: dict[str, Any] = {}
68
+ for k, v in d.items():
69
+ if k not in self._field_names:
70
+ continue
71
+ serialized = _serialize_value(v)
72
+ if serialized is not None:
73
+ result[k] = serialized
74
+ return result
75
+
76
+ def from_item(self, item: Mapping[str, Any]) -> T:
77
+ data: dict[str, Any] = {}
78
+ for f in fields(self._entity_type):
79
+ if f.name in item:
80
+ val = item[f.name]
81
+ if f.type in ("UUID", "uuid.UUID") or (
82
+ isinstance(f.type, str) and "UUID" in f.type
83
+ ):
84
+ data[f.name] = UUID(val) if isinstance(val, str) else val
85
+ elif f.type in ("datetime", "datetime.datetime") or (
86
+ isinstance(f.type, str) and "datetime" in f.type.lower()
87
+ and "date" != f.type.lower()
88
+ ):
89
+ data[f.name] = (
90
+ datetime.fromisoformat(val)
91
+ if isinstance(val, str)
92
+ else _deserialize_value(val)
93
+ )
94
+ elif f.type in ("date", "datetime.date"):
95
+ data[f.name] = (
96
+ date.fromisoformat(val)
97
+ if isinstance(val, str)
98
+ else _deserialize_value(val)
99
+ )
100
+ else:
101
+ data[f.name] = _deserialize_value(val)
102
+ return self._entity_type(**data)
103
+
104
+ def unique_fields(self) -> dict[str, str]:
105
+ return self._unique_fields
106
+
107
+ def has_attr(self, name: str) -> bool:
108
+ return name in self._field_names
109
+
110
+ def attr_to_storage(self, name: str) -> str:
111
+ return name
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Sequence, TypeVar, Generic, Hashable, Mapping, Any
4
+ try:
5
+ from typing import override
6
+ except ImportError:
7
+ from typing_extensions import override
8
+
9
+ from decimal import Decimal
10
+
11
+ import boto3
12
+ from boto3.dynamodb.conditions import Key, Attr, ConditionBase
13
+ from persistence_kit.contracts.repository import Repository
14
+ from persistence_kit.repository.dynamodb_repo.dynamodb_mapper import DynamoMapper, _serialize_value
15
+ from persistence_kit.repository.filter_ops import (
16
+ is_logical_key,
17
+ iter_criteria_groups,
18
+ is_multi_value,
19
+ is_range_dict,
20
+ iter_range_ops,
21
+ )
22
+
23
+ T = TypeVar("T")
24
+ TId = TypeVar("TId", bound=Hashable)
25
+
26
+
27
+ def _range_to_condition(attr: Attr, value: Mapping[str, Any]) -> ConditionBase | None:
28
+ conditions: list[ConditionBase] = []
29
+ for op, v in iter_range_ops(value):
30
+ v = _serialize_value(v)
31
+ if op == "between":
32
+ conditions.append(attr.between(_serialize_value(v[0]), _serialize_value(v[1])))
33
+ elif op == "gte":
34
+ conditions.append(attr.gte(v))
35
+ elif op == "gt":
36
+ conditions.append(attr.gt(v))
37
+ elif op == "lte":
38
+ conditions.append(attr.lte(v))
39
+ elif op == "lt":
40
+ conditions.append(attr.lt(v))
41
+ elif op == "in":
42
+ conditions.append(attr.is_in([_serialize_value(i) for i in v]))
43
+ elif op == "eq":
44
+ conditions.append(attr.eq(v))
45
+ elif op == "ne":
46
+ conditions.append(attr.ne(v))
47
+ elif op == "contains":
48
+ conditions.append(attr.contains(str(v)))
49
+ elif op == "icontains":
50
+ conditions.append(attr.contains(str(v).lower()) | attr.contains(str(v).upper()) | attr.contains(str(v)))
51
+ elif op == "startswith" or op == "istartswith":
52
+ conditions.append(attr.begins_with(str(v)))
53
+ elif op == "endswith" or op == "iendswith":
54
+ conditions.append(attr.contains(str(v)))
55
+ if not conditions:
56
+ return None
57
+ result = conditions[0]
58
+ for c in conditions[1:]:
59
+ result = result & c
60
+ return result
61
+
62
+
63
+ def _build_filter(
64
+ mapper: DynamoMapper,
65
+ criteria: Mapping[str, Hashable | list[Hashable] | Mapping[str, Any]],
66
+ ) -> ConditionBase | None:
67
+ if not criteria:
68
+ return None
69
+ for v in criteria.values():
70
+ if is_multi_value(v) and not v:
71
+ return False
72
+ if is_range_dict(v) and v.get("in") == []:
73
+ return False
74
+
75
+ conditions: list[ConditionBase] = []
76
+ for k, v in criteria.items():
77
+ if is_logical_key(k):
78
+ group_conditions: list[ConditionBase] = []
79
+ for group in iter_criteria_groups(v):
80
+ rendered = _build_filter(mapper, group)
81
+ if rendered is False:
82
+ continue
83
+ if rendered is not None:
84
+ group_conditions.append(rendered)
85
+ if not group_conditions:
86
+ return False
87
+ combined = group_conditions[0]
88
+ for gc in group_conditions[1:]:
89
+ if k == "or":
90
+ combined = combined | gc
91
+ else:
92
+ combined = combined & gc
93
+ conditions.append(combined)
94
+ continue
95
+
96
+ field = mapper.attr_to_storage(k)
97
+ attr = Attr(field)
98
+ if v is None:
99
+ conditions.append(attr.not_exists() | attr.eq(None))
100
+ elif is_multi_value(v):
101
+ conditions.append(attr.is_in([_serialize_value(i) for i in v]))
102
+ elif is_range_dict(v):
103
+ range_cond = _range_to_condition(attr, v)
104
+ if range_cond is None:
105
+ return False
106
+ conditions.append(range_cond)
107
+ else:
108
+ conditions.append(attr.eq(_serialize_value(v)))
109
+
110
+ if not conditions:
111
+ return None
112
+ result = conditions[0]
113
+ for c in conditions[1:]:
114
+ result = result & c
115
+ return result
116
+
117
+
118
+ class DynamoRepository(Repository[T, TId], Generic[T, TId]):
119
+ def __init__(
120
+ self,
121
+ table_name: str,
122
+ mapper: DynamoMapper,
123
+ region: str = "us-east-1",
124
+ ) -> None:
125
+ self._mapper = mapper
126
+ self._dynamodb = boto3.resource("dynamodb", region_name=region)
127
+ self._table = self._dynamodb.Table(table_name)
128
+
129
+ @override
130
+ async def add(self, entity: T) -> None:
131
+ item = self._mapper.to_item(entity)
132
+ self._table.put_item(
133
+ Item=item,
134
+ ConditionExpression=Attr("id").not_exists(),
135
+ )
136
+
137
+ @override
138
+ async def get(self, entity_id: TId) -> Optional[T]:
139
+ response = self._table.get_item(Key={"id": _serialize_value(entity_id)})
140
+ item = response.get("Item")
141
+ return self._mapper.from_item(item) if item else None
142
+
143
+ @override
144
+ async def list(
145
+ self,
146
+ *,
147
+ offset: int = 0,
148
+ limit: int = 50,
149
+ sort_by: str | None = None,
150
+ sort_desc: bool = False,
151
+ ) -> Sequence[T]:
152
+ items = self._scan_all()
153
+ entities = [self._mapper.from_item(i) for i in items]
154
+ if sort_by is not None:
155
+ if not self._mapper.has_attr(sort_by):
156
+ raise ValueError(f"Invalid sort attribute: {sort_by}")
157
+ entities.sort(key=lambda e: getattr(e, sort_by, None) or "", reverse=sort_desc)
158
+ return entities[offset : offset + limit]
159
+
160
+ @override
161
+ async def update(self, entity: T) -> None:
162
+ item = self._mapper.to_item(entity)
163
+ self._table.put_item(Item=item)
164
+
165
+ @override
166
+ async def delete(self, entity_id: TId) -> None:
167
+ self._table.delete_item(Key={"id": _serialize_value(entity_id)})
168
+
169
+ @override
170
+ async def get_by_index(self, index: str, value: Hashable) -> Optional[T]:
171
+ field = self._mapper.unique_fields().get(index)
172
+ if not field:
173
+ return None
174
+ response = self._table.scan(
175
+ FilterExpression=Attr(field).eq(_serialize_value(value)),
176
+ Limit=1,
177
+ )
178
+ items = response.get("Items", [])
179
+ return self._mapper.from_item(items[0]) if items else None
180
+
181
+ @override
182
+ async def count(self) -> int:
183
+ return self._table.item_count
184
+
185
+ @override
186
+ async def count_by_fields(
187
+ self,
188
+ criteria: Mapping[str, Hashable | list[Hashable] | Mapping[str, Any]],
189
+ ) -> int:
190
+ if not criteria:
191
+ return await self.count()
192
+ filter_expr = _build_filter(self._mapper, criteria)
193
+ if filter_expr is False:
194
+ return 0
195
+ items = self._scan_with_filter(filter_expr)
196
+ return len(items)
197
+
198
+ @override
199
+ async def list_by_fields(
200
+ self,
201
+ criteria: Mapping[str, Hashable | list[Hashable] | Mapping[str, Any]],
202
+ *,
203
+ offset: int = 0,
204
+ limit: Optional[int] = 50,
205
+ sort_by: str | None = None,
206
+ sort_desc: bool = False,
207
+ ) -> Sequence[T]:
208
+ if not criteria:
209
+ return []
210
+ filter_expr = _build_filter(self._mapper, criteria)
211
+ if filter_expr is False:
212
+ return []
213
+ items = self._scan_with_filter(filter_expr)
214
+ entities = [self._mapper.from_item(i) for i in items]
215
+ if sort_by is not None:
216
+ if not self._mapper.has_attr(sort_by):
217
+ raise ValueError(f"Invalid sort attribute: {sort_by}")
218
+ entities.sort(key=lambda e: getattr(e, sort_by, None) or "", reverse=sort_desc)
219
+ end = offset + limit if limit is not None else None
220
+ return entities[offset:end]
221
+
222
+ @override
223
+ async def distinct_values(
224
+ self,
225
+ field: str,
226
+ criteria: Mapping[str, Hashable | list[Hashable] | Mapping[str, Any]] | None = None,
227
+ ) -> Sequence[Any]:
228
+ if not self._mapper.has_attr(field):
229
+ raise ValueError(f"Invalid distinct attribute: {field}")
230
+ storage_field = self._mapper.attr_to_storage(field)
231
+ if criteria:
232
+ filter_expr = _build_filter(self._mapper, criteria)
233
+ if filter_expr is False:
234
+ return []
235
+ items = self._scan_with_filter(filter_expr)
236
+ else:
237
+ items = self._scan_all()
238
+ seen: set = set()
239
+ result: list[Any] = []
240
+ for item in items:
241
+ val = item.get(storage_field)
242
+ if val is not None and val not in seen:
243
+ seen.add(val)
244
+ result.append(val)
245
+ return result
246
+
247
+ def _scan_all(self) -> list[dict]:
248
+ items: list[dict] = []
249
+ response = self._table.scan()
250
+ items.extend(response.get("Items", []))
251
+ while "LastEvaluatedKey" in response:
252
+ response = self._table.scan(ExclusiveStartKey=response["LastEvaluatedKey"])
253
+ items.extend(response.get("Items", []))
254
+ return items
255
+
256
+ def _scan_with_filter(self, filter_expr: ConditionBase | None) -> list[dict]:
257
+ items: list[dict] = []
258
+ kwargs: dict[str, Any] = {}
259
+ if filter_expr is not None:
260
+ kwargs["FilterExpression"] = filter_expr
261
+ response = self._table.scan(**kwargs)
262
+ items.extend(response.get("Items", []))
263
+ while "LastEvaluatedKey" in response:
264
+ kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
265
+ response = self._table.scan(**kwargs)
266
+ items.extend(response.get("Items", []))
267
+ return items
@@ -10,6 +10,8 @@ from persistence_kit.contracts.view_repository import ViewRepository
10
10
  from persistence_kit.settings.constants import Database
11
11
  from persistence_kit.settings.repo_settings import RepoSettings
12
12
  from persistence_kit.repository.memory_repo.memory_repo import MemoryRepository
13
+ from persistence_kit.repository.dynamodb_repo.dynamodb_mapper import DynamoMapper
14
+ from persistence_kit.repository.dynamodb_repo.dynamodb_repo import DynamoRepository
13
15
  from persistence_kit.repository.mongo_repo.mongo_mapper import DataclassMapper
14
16
  from persistence_kit.repository.mongo_repo.mongo_repo import MongoRepository
15
17
  from persistence_kit.repository.sqlalchemy_repo.sqlalchemy_dataclass_mapper import SqlDataclassMapper
@@ -81,6 +83,21 @@ def _repo_cached(entity_key: str, resolved: Database) -> Repository[Any, UUID]:
81
83
  )
82
84
  return MongoRepository[Any, UUID](db, mapper)
83
85
 
86
+ if resolved is Database.DYNAMODB:
87
+ prefix = settings.dynamodb_table_prefix
88
+ table_name = f"{prefix}{config['collection']}" if prefix else config["collection"]
89
+ dynamo_mapper = DynamoMapper(
90
+ entity_type,
91
+ table_name,
92
+ unique_fields={
93
+ key: (value if isinstance(value, str) else key)
94
+ for key, value in (config.get("unique") or {}).items()
95
+ },
96
+ )
97
+ return DynamoRepository[Any, UUID](
98
+ table_name, dynamo_mapper, region=settings.dynamodb_region
99
+ )
100
+
84
101
  if resolved is Database.POSTGRES:
85
102
  engine = get_engine()
86
103
  table_name = config["collection"]
@@ -5,3 +5,4 @@ class Database(str, Enum):
5
5
  MEMORY = "memory"
6
6
  MONGO = "mongo"
7
7
  POSTGRES = "postgres"
8
+ DYNAMODB = "dynamodb"
@@ -18,6 +18,9 @@ class RepoSettings(BaseSettings):
18
18
  postgres_port: int | None = 5432
19
19
  postgres_db: str | None = None
20
20
 
21
+ dynamodb_table_prefix: str = ""
22
+ dynamodb_region: str = "us-east-1"
23
+
21
24
  @field_validator("cors_origins", mode="before")
22
25
  def split_origins(cls, value):
23
26
  return split_csv_list(value)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "persistence-kit"
3
- version = "2.0.0"
3
+ version = "3.0.0"
4
4
  description = "Reusable persistence and repository toolkit"
5
5
  authors = ["Andres Felipe Serrano Barrios <andresfserrano1@gmail.com>"]
6
6
  readme = "README.md"
@@ -31,6 +31,7 @@ asyncpg = "^0.30.0"
31
31
  typing-extensions = "^4.12.0"
32
32
  pydantic-settings = "^2.3.0"
33
33
  fastapi = "^0.115.0"
34
+ boto3 = "^1.35.0"
34
35
 
35
36
  [tool.poetry.group.dev.dependencies]
36
37
  pytest = "^8.3.0"
File without changes