persistence-kit 3.9.1__tar.gz → 3.11.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 (102) hide show
  1. persistence_kit-3.11.0/PKG-INFO +476 -0
  2. persistence_kit-3.11.0/README.md +431 -0
  3. persistence_kit-3.11.0/persistence_kit/cache/dynamodb.py +97 -0
  4. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/cache/factory.py +17 -1
  5. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/factory.py +11 -0
  6. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/ports.py +2 -0
  7. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/providers/cognito_identity_provider.py +210 -56
  8. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/providers/memory_security_provider.py +20 -10
  9. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/settings/app_settings.py +3 -0
  10. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/settings/cache_settings.py +1 -0
  11. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/pyproject.toml +1 -1
  12. persistence_kit-3.9.1/PKG-INFO +0 -401
  13. persistence_kit-3.9.1/README.md +0 -356
  14. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/LICENSE +0 -0
  15. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/__init__.py +0 -0
  16. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/api/__init__.py +0 -0
  17. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/api/common.py +0 -0
  18. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/api/error_handlers.py +0 -0
  19. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/api/exceptions.py +0 -0
  20. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/api/rate_limit.py +0 -0
  21. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/api/route_loader.py +0 -0
  22. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/authenticated_user.py +0 -0
  23. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/bootstrap/__init__.py +0 -0
  24. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/bootstrap/configuration.py +0 -0
  25. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/bootstrap/seeders.py +0 -0
  26. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/bootstrap/startup.py +0 -0
  27. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/cache/__init__.py +0 -0
  28. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/cache/contracts.py +0 -0
  29. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/cache/memory.py +0 -0
  30. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/cache/mongo.py +0 -0
  31. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/cache/namespaced.py +0 -0
  32. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/contracts/__init__.py +0 -0
  33. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/contracts/repository.py +0 -0
  34. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/contracts/view_repository.py +0 -0
  35. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/py.typed +0 -0
  36. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/__init__.py +0 -0
  37. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +0 -0
  38. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +0 -0
  39. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/filter_ops.py +0 -0
  40. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/memory_repo/__init__.py +0 -0
  41. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
  42. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
  43. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
  44. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/mongo_repo/mongo_repo.py +0 -0
  45. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
  46. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
  47. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
  48. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +0 -0
  49. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +0 -0
  50. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
  51. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/__init__.py +0 -0
  52. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/factory/__init__.py +0 -0
  53. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/factory/repository_factory.py +0 -0
  54. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/registry/__init__.py +0 -0
  55. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
  56. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/view/__init__.py +0 -0
  57. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
  58. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/resilience/__init__.py +0 -0
  59. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/resilience/circuit.py +0 -0
  60. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/__init__.py +0 -0
  61. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/aggregate.py +0 -0
  62. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/__init__.py +0 -0
  63. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/api_key.py +0 -0
  64. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/base.py +0 -0
  65. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/basic.py +0 -0
  66. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/bearer.py +0 -0
  67. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/login.py +0 -0
  68. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/auth/oauth2.py +0 -0
  69. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/caching.py +0 -0
  70. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/client.py +0 -0
  71. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/config.py +0 -0
  72. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/contracts.py +0 -0
  73. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/errors.py +0 -0
  74. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/factory.py +0 -0
  75. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/mapping.py +0 -0
  76. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/memory.py +0 -0
  77. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/payload.py +0 -0
  78. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/populate.py +0 -0
  79. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/provider.py +0 -0
  80. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/registry.py +0 -0
  81. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/resolver.py +0 -0
  82. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/restclient/retry.py +0 -0
  83. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/__init__.py +0 -0
  84. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/providers/__init__.py +0 -0
  85. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/registration.py +0 -0
  86. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/token_verifiers/__init__.py +0 -0
  87. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/token_verifiers/cognito_jwt_verifier.py +0 -0
  88. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/security/token_verifiers/memory_jwt_verifier.py +0 -0
  89. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/settings/__init__.py +0 -0
  90. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/settings/constants.py +0 -0
  91. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/settings/parsers.py +0 -0
  92. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/settings/repo_settings.py +0 -0
  93. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/__init__.py +0 -0
  94. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/contracts.py +0 -0
  95. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/errors.py +0 -0
  96. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/factory.py +0 -0
  97. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/local.py +0 -0
  98. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/media.py +0 -0
  99. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/routes.py +0 -0
  100. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/storage/s3.py +0 -0
  101. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/utils/__init__.py +0 -0
  102. {persistence_kit-3.9.1 → persistence_kit-3.11.0}/persistence_kit/utils/upsert.py +0 -0
@@ -0,0 +1,476 @@
1
+ Metadata-Version: 2.4
2
+ Name: persistence-kit
3
+ Version: 3.11.0
4
+ Summary: Reusable persistence and repository toolkit
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: repository,persistence,mongodb,sqlalchemy,async
8
+ Author: Andres Felipe Serrano Barrios
9
+ Author-email: andresfserrano1@gmail.com
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Database
20
+ Classifier: Typing :: Typed
21
+ Provides-Extra: all
22
+ Provides-Extra: api
23
+ Provides-Extra: dynamodb
24
+ Provides-Extra: restclient
25
+ Provides-Extra: security
26
+ Provides-Extra: security-cognito
27
+ Provides-Extra: storage-routes
28
+ Provides-Extra: storage-s3
29
+ Provides-Extra: testing
30
+ Requires-Dist: asyncpg (>=0.30.0,<1.0.0)
31
+ Requires-Dist: boto3 (>=1.35.0,<2.0.0) ; extra == "storage-s3" or extra == "security-cognito" or extra == "dynamodb" or extra == "all"
32
+ Requires-Dist: fastapi (>=0.115.0,<1.0.0) ; extra == "api" or extra == "storage-routes" or extra == "security" or extra == "security-cognito" or extra == "testing" or extra == "all"
33
+ Requires-Dist: httpx (>=0.28.0,<1.0.0) ; extra == "restclient" or extra == "testing" or extra == "all"
34
+ Requires-Dist: motor (>=3.7.1,<4.0.0)
35
+ Requires-Dist: pydantic-settings (>=2.3.0,<3.0.0)
36
+ Requires-Dist: pyjwt[crypto] (>=2.10.1,<3.0.0) ; extra == "security" or extra == "security-cognito" or extra == "testing" or extra == "all"
37
+ Requires-Dist: python-multipart (>=0.0.31,<1.0.0) ; extra == "api" or extra == "storage-routes" or extra == "security" or extra == "security-cognito" or extra == "testing" or extra == "all"
38
+ Requires-Dist: sqlalchemy[asyncio] (>=2.0.43,<3.0.0)
39
+ Requires-Dist: typing-extensions (>=4.12.0,<5.0.0)
40
+ Project-URL: Documentation, https://github.com/andresfserrano/persistence-kit#readme
41
+ Project-URL: Homepage, https://pypi.org/project/persistence-kit/
42
+ Project-URL: Repository, https://github.com/andresfserrano/persistence-kit
43
+ Description-Content-Type: text/markdown
44
+
45
+ # persistence-kit
46
+
47
+ The plumbing every one of our services rewrites: repositories, settings, object
48
+ storage, authentication, an HTTP client and a cache. You describe your entities
49
+ once and the kit gives you an async repository for them, backed by memory,
50
+ MongoDB, PostgreSQL or DynamoDB, without your domain code knowing which one.
51
+
52
+ Nothing here knows about your business. Roles, permissions, and domain rules stay
53
+ in your application.
54
+
55
+ - Python 3.11+ · async everywhere · type hints shipped (`py.typed`)
56
+ - Optional dependencies stay out of the graph until you ask for them
57
+
58
+ ## Five minutes
59
+
60
+ ```bash
61
+ pip install persistence-kit
62
+ ```
63
+
64
+ ```python
65
+ from dataclasses import dataclass, field
66
+ from uuid import UUID, uuid4
67
+
68
+ from persistence_kit import Database
69
+ from persistence_kit.repository_factory import get_repo, register_entity
70
+
71
+
72
+ @dataclass
73
+ class User:
74
+ email: str
75
+ name: str
76
+ id: UUID = field(default_factory=uuid4)
77
+
78
+
79
+ register_entity("user", {
80
+ "entity": User,
81
+ "collection": "users",
82
+ "database": Database.MEMORY,
83
+ "unique": {"email": "email"},
84
+ })
85
+
86
+ repo = get_repo("user")
87
+ await repo.add(User(email="ada@example.org", name="Ada"))
88
+ ada = await repo.get_by_index("email", "ada@example.org")
89
+ ```
90
+
91
+ The repository protocol is small and the same for every backend: `add`, `get`,
92
+ `update`, `delete`, `get_by_index`, `list`, `list_by_fields`, `count` and
93
+ `count_by_fields`.
94
+
95
+ Switch `database` to `Database.POSTGRES` and the same code talks to PostgreSQL.
96
+ That is the whole point of the kit.
97
+
98
+ ## The one idea: the entity registry
99
+
100
+ Everything else hangs off a single dictionary. You register each entity once,
101
+ usually in a `register_defaults()` function in your app, and from then on you ask
102
+ for repositories by key.
103
+
104
+ ```python
105
+ from persistence_kit.repository_factory import set_registry_initializer
106
+
107
+ set_registry_initializer(register_defaults) # called during startup
108
+ ```
109
+
110
+ What goes in a registration:
111
+
112
+ | Key | Meaning |
113
+ | --- | --- |
114
+ | `entity` | The dataclass this key maps to |
115
+ | `collection` | Table or collection name in the backing store |
116
+ | `database` | `Database.MEMORY`, `MONGO`, `POSTGRES` or `DYNAMODB`. Defaults to `REPO_DATABASE` |
117
+ | `unique` | Indexed lookups, `{"index_name": "field"}`, used by `get_by_index` |
118
+ | `relations` | How this entity joins to others, including many-to-many through a pivot |
119
+
120
+ Relations are what `get_repo_view(...)` reads to return an entity with its
121
+ related rows already populated, so you do not write joins by hand. The shapes and
122
+ the traps are in [`docs/repositories_and_relations.md`](docs/repositories_and_relations.md).
123
+
124
+ Four ways to reach a repository, all equivalent:
125
+
126
+ ```python
127
+ get_repo("user") # plain repository
128
+ get_repo_view("user") # repository that populates relations
129
+ provide_repo("user") # FastAPI dependency
130
+ provide_view_repo("user") # FastAPI dependency, populated
131
+ ```
132
+
133
+ ## Installation and extras
134
+
135
+ The base install stays light. Each capability that needs a heavy dependency
136
+ lives behind an extra, so a service that only talks to Mongo never installs
137
+ `boto3` or `fastapi`.
138
+
139
+ ```bash
140
+ pip install "persistence-kit[api,security,restclient]"
141
+ ```
142
+
143
+ | Extra | Gives you | Pulls in |
144
+ | --- | --- | --- |
145
+ | `api` | FastAPI exceptions, pagination, route loading, error handlers | fastapi, python-multipart |
146
+ | `security` | Memory identity provider and JWT verifier | fastapi, pyjwt, python-multipart |
147
+ | `security-cognito` | The above plus AWS Cognito and JWKS | + boto3 |
148
+ | `storage-s3` | S3 object storage adapter | boto3 |
149
+ | `storage-routes` | FastAPI route to serve local exports | fastapi, python-multipart |
150
+ | `dynamodb` | DynamoDB repository backend | boto3 |
151
+ | `restclient` | The REST client and its cache | httpx |
152
+ | `testing` | Everything the test suite needs | fastapi, pyjwt, httpx, python-multipart |
153
+ | `all` | Every optional capability | all of the above |
154
+
155
+ Importing `persistence_kit` never loads an optional dependency by itself. Ask for
156
+ something you did not install and you get a message telling you which extra to
157
+ add, not an obscure `ImportError`.
158
+
159
+ ## What is in the box
160
+
161
+ | Module | What it does |
162
+ | --- | --- |
163
+ | `contracts/` | The protocols: `Repository`, `ViewRepository`, `ObjectStorage`, `IdentityProvider` |
164
+ | `repository/` | One implementation per backend: memory, Mongo, SQLAlchemy, DynamoDB |
165
+ | `repository_factory/` | The entity registry, the factory, and the view repository that populates relations |
166
+ | `settings/` | `RepoSettings` and `PersistenceKitSettings`, plus shared enums and parsers |
167
+ | `storage/` | Object storage: local directory or S3, with presigned URLs |
168
+ | `security/` | Identity providers and JWT verifiers, memory or Cognito |
169
+ | `restclient/` | HTTP client with pluggable auth, endpoint resolution, retries and DTO decoding |
170
+ | `cache/` | Key-value cache with TTL: memory, Mongo or DynamoDB |
171
+ | `resilience/` | Circuit breaker, shared by anything that calls out |
172
+ | `api/` | Reusable FastAPI pieces: error handlers, pagination, rate limiting |
173
+ | `bootstrap/` | Startup helpers, configuration registry, seed orchestration |
174
+ | `utils/` | Small transversal helpers such as upserts |
175
+
176
+ Import from `persistence_kit` when the public facade is enough. Reach into a
177
+ subpackage only when you need something implementation-specific. The root
178
+ exports about a hundred names, so let your editor complete them rather than
179
+ keeping a list here.
180
+
181
+ ## Object storage
182
+
183
+ Writing generated files without your domain layer knowing where they land.
184
+
185
+ ```python
186
+ from persistence_kit.storage import LocalObjectStorage
187
+
188
+ storage = LocalObjectStorage(
189
+ base_dir=".local",
190
+ public_base_url="http://localhost:8000",
191
+ signing_secret="dev-secret",
192
+ )
193
+
194
+ key = await storage.upload("exports/report.csv", b"id,name\n1,Ada\n", "text/csv")
195
+ url = await storage.generate_presigned_url(key)
196
+ ```
197
+
198
+ `LocalObjectStorage` writes to a directory and signs its own download URLs.
199
+ `S3ObjectStorage` uploads to S3 and returns AWS presigned URLs. Pick one with
200
+ `get_export_storage(settings)`, which reads your settings and caches the adapter.
201
+
202
+ FastAPI apps can mount `build_local_export_storage_router(...)` to serve local
203
+ files, passing their settings provider, an optional current-user dependency, and
204
+ an authorization callback.
205
+
206
+ Needs `[storage-s3]` for S3 and `[storage-routes]` for the route.
207
+
208
+ ## Security
209
+
210
+ ```python
211
+ from persistence_kit.security import MemorySecurityProvider, MemoryJwtVerifier
212
+
213
+ identity = MemorySecurityProvider(
214
+ jwt_secret="dev-secret-with-enough-length",
215
+ jwt_issuer="local-sandbox",
216
+ seed_role_users=True,
217
+ seed_role_codes=("admin", "operator"),
218
+ seed_user_domain="example.org",
219
+ )
220
+ verifier = MemoryJwtVerifier(secret="dev-secret-with-enough-length", issuer="local-sandbox")
221
+ ```
222
+
223
+ Two protocols, `IdentityProvider` and `TokenVerifier`, with a memory pair for
224
+ tests and local work and a Cognito pair for real deployments. Registration,
225
+ login, and password-reset results come back as dataclasses, not raw dicts.
226
+ `get_identity_provider(settings)` and `get_token_verifier(settings)` build the
227
+ right pair from your settings.
228
+
229
+ Your roles, authorization policies and route permission matrices stay in your
230
+ application. The kit only answers "who is this".
231
+
232
+ Needs `[security]`, or `[security-cognito]` for Cognito.
233
+
234
+ ## REST client and cache
235
+
236
+ For calling somebody else's HTTP API without writing the same client again.
237
+ Register a service once, then ask for its client:
238
+
239
+ ```python
240
+ from persistence_kit.restclient import get_rest_client, register_rest_service, ServiceConfig
241
+
242
+ register_rest_service(
243
+ "billing",
244
+ base_url="https://billing.example.org/api",
245
+ config=ServiceConfig(
246
+ default_headers={"X-Api-Key": settings.billing_key},
247
+ timeout_seconds=10,
248
+ cacheable=True,
249
+ cache_ttl_seconds=300,
250
+ ),
251
+ )
252
+
253
+ client = get_rest_client("billing")
254
+ ```
255
+
256
+ It brings pluggable authentication (`NoAuth`, `ApiKeyAuth`, `BasicAuth`,
257
+ `BearerAuth`, `OAuth2ClientCredentials`, `LoginTokenAuth`), endpoint resolution
258
+ that can read a service directory, a retry policy, DTO decoding through
259
+ `decode`, and optional response caching. `MemoryRestClient` stands in for the
260
+ real one in tests.
261
+
262
+ Base URLs can be overridden per environment with `REST_SERVICE_URLS`, a JSON map
263
+ of service name to URL, without touching the registration.
264
+
265
+ The cache is a plain key-value store with TTL, chosen by `CACHE_BACKEND` the
266
+ same way repositories are chosen by `REPO_DATABASE`. Set `CACHE_NAMESPACE` when
267
+ several apps share one DynamoDB table or Mongo collection so their keys do not
268
+ collide.
269
+
270
+ Both are covered in depth in [`docs/restclient_and_cache.md`](docs/restclient_and_cache.md).
271
+
272
+ Needs `[restclient]`.
273
+
274
+ ## Resilience
275
+
276
+ `CircuitBreaker` is domain-agnostic and shared by anything that calls out: after
277
+ enough consecutive failures it opens, fails fast with `CircuitOpenError` instead
278
+ of hammering a service that is already down, and closes again after a cooldown.
279
+ `RetryPolicy`, which lives with the REST client, handles the retry side.
280
+
281
+ ## Settings
282
+
283
+ `PersistenceKitSettings` gathers what most services need: auth, storage,
284
+ observability, AWS and job-service settings. Inherit from it and override only
285
+ what is yours.
286
+
287
+ ```python
288
+ from persistence_kit import Database, PersistenceKitSettings
289
+
290
+
291
+ class Settings(PersistenceKitSettings):
292
+ service_name: str = "my-api"
293
+ key_status_history_database: Database = Database.MONGO
294
+ memory_seed_role_codes: tuple[str, ...] = ("admin", "operator")
295
+ ```
296
+
297
+ The factories read that object: `get_identity_provider`, `get_token_verifier`,
298
+ `get_export_storage`, `get_cache`.
299
+
300
+ ### Environment variables
301
+
302
+ | Variable | Meaning |
303
+ | --- | --- |
304
+ | `REPO_DATABASE` | `memory`, `mongo`, `postgres` or `dynamodb`. Default backend for every entity |
305
+ | `CACHE_BACKEND` | `memory`, `mongo` or `dynamodb`. Defaults to `memory` |
306
+ | `CACHE_NAMESPACE` | Key prefix, so apps sharing a cache store do not collide |
307
+ | `REST_SERVICE_URLS` | JSON map `{"service": "base_url"}` overriding registered REST base URLs |
308
+ | `MONGO_DSN`, `MONGO_DB` | Mongo connection |
309
+ | `POSTGRES_DSN` | Full DSN, if you prefer it to the pieces below |
310
+ | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB` | Postgres connection |
311
+ | `POSTGRES_SSL` | Set when the server requires TLS |
312
+ | `DYNAMODB_REGION`, `DYNAMODB_TABLE_PREFIX` | DynamoDB connection |
313
+
314
+ ## Wiring it into an application
315
+
316
+ 1. Define your entities as dataclasses.
317
+ 2. Register them in a `register_defaults()` in your app.
318
+ 3. Call `set_registry_initializer(register_defaults)` at startup.
319
+ 4. Resolve repositories with `get_repo(...)`, `get_repo_view(...)` or the FastAPI providers.
320
+ 5. Use `ConfigRegistry` and `SeederProvider` as bootstrap infrastructure only. What gets registered and seeded stays in your app.
321
+
322
+ ## Trying a change against an app on your machine
323
+
324
+ This is the everyday loop, and it needs no release, no PR and no TestPyPI. It
325
+ assumes the kit and the app sit next to each other:
326
+
327
+ ```
328
+ aux programación udea/
329
+ ├── persistence_kit/ <- this repository
330
+ └── api_store_manager_v1/ <- the app that consumes it
331
+ ```
332
+
333
+ Install the kit into the app's environment in editable mode, from the app:
334
+
335
+ ```bash
336
+ cd api_store_manager_v1
337
+ .venv/Scripts/pip install -e ../persistence_kit # Linux/macOS: .venv/bin/pip
338
+ ```
339
+
340
+ From that moment the app imports your working copy. Edit the kit, rerun the app
341
+ or its tests, and the change is already there. No reinstall between edits.
342
+
343
+ To go back to the published version:
344
+
345
+ ```bash
346
+ .venv/Scripts/pip uninstall persistence-kit
347
+ poetry install
348
+ ```
349
+
350
+ Two things that will confuse you if nobody warns you:
351
+
352
+ - **The reported version lies.** In editable mode `importlib.metadata.version("persistence-kit")`
353
+ keeps returning whatever the old `dist-info` said, even though the code running
354
+ is your source. Trust `persistence_kit.__file__`, not the version string.
355
+ - **The app pins an exact version.** `api_store_manager_v1` asks for a pinned
356
+ `persistence-kit` with its extras, so `poetry install` will happily undo your
357
+ editable install. Redo it after any dependency change.
358
+
359
+ Run the kit's own tests with the app's interpreter, which already has every extra
360
+ installed:
361
+
362
+ ```bash
363
+ "../api_store_manager_v1/.venv/Scripts/python.exe" -m pytest -q
364
+ ```
365
+
366
+ Only when the change has to be tried from a machine that is not yours does it
367
+ make sense to publish a preview, which is the next section.
368
+
369
+ ## Adding something to the kit
370
+
371
+ Every capability here is built the same way. Follow the shape and your module
372
+ will look like it was always part of the kit.
373
+
374
+ **1. Start with the contract.** Declare what the thing does before writing how.
375
+ The repository pair uses `ABC`; everything else uses `Protocol`, so an
376
+ implementation only has to match the shape, not inherit from anything.
377
+
378
+ ```python
379
+ from typing import Protocol
380
+
381
+
382
+ class Mailer(Protocol):
383
+ async def send(self, *, to: str, subject: str, body: str) -> None: ...
384
+ ```
385
+
386
+ Everything is `async` and arguments are keyword-only. Both rules exist so callers
387
+ read clearly at the call site and so adding a parameter never breaks anyone.
388
+
389
+ **2. Write two implementations, not one.** A memory one and a real one. The
390
+ memory one is not a toy: it is what tests and local development run against, and
391
+ having it forces the contract to stay honest. `InMemoryTTLCache` next to the
392
+ Mongo cache, `MemorySecurityProvider` next to Cognito, `MemoryRestClient` next to
393
+ the httpx one.
394
+
395
+ **3. Choose between them with a factory driven by settings.** Never with an `if`
396
+ scattered around the app. The factory reads an enum from settings, caches with
397
+ `lru_cache`, and imports the provider lazily inside the builder so an unused
398
+ backend never loads its dependency.
399
+
400
+ ```python
401
+ @lru_cache
402
+ def get_mailer(settings):
403
+ if settings.mailer_backend is MailerBackend.SES:
404
+ from persistence_kit.mailer.ses import SesMailer # imported only if used
405
+ return SesMailer(region=settings.aws_region)
406
+ return InMemoryMailer()
407
+ ```
408
+
409
+ **4. Give the package its own error hierarchy.** One base exception and specific
410
+ subclasses, the way `storage/errors.py` and `restclient/errors.py` do it. Callers
411
+ should be able to catch the whole family or one precise case.
412
+
413
+ **5. Export it lazily.** If it needs an optional dependency, add it to
414
+ `_OPTIONAL_EXPORTS` in the root `__init__.py` and declare its extra in
415
+ `pyproject.toml`. Someone who imports `persistence_kit` without that extra must
416
+ get a message naming the extra, not an `ImportError`. `tests/test_capabilities.py`
417
+ enforces this and will fail if you skip it.
418
+
419
+ **6. Add tests next to the feature.** Async tests carry `@pytest.mark.asyncio`.
420
+
421
+ **Getting it merged.** `main` is protected, so it goes through a pull request.
422
+ Branch off `main`, keep the change to one capability, run the suite, and open the
423
+ PR. Anything domain-specific, roles, business rules, product settings, belongs in
424
+ the application that consumes the kit, not here.
425
+
426
+ ## Development
427
+
428
+ From the kit's root, with its own environment:
429
+
430
+ ```bash
431
+ poetry lock
432
+ poetry install --with dev --all-extras
433
+ poetry run pytest -q
434
+ ```
435
+
436
+ Current baseline: **434 tests passing** (version 3.9.1). Async tests use
437
+ `pytest-asyncio` in strict mode, so each one carries `@pytest.mark.asyncio`.
438
+
439
+ `tests/test_capabilities.py` guards the lazy-import promise: it fails if merely
440
+ importing `persistence_kit` drags in fastapi, pyjwt or boto3.
441
+
442
+ ## Releasing
443
+
444
+ The normal path, and the only one that produces an official version:
445
+
446
+ 1. Merge to `main`. The branch is protected, so it goes through a pull request.
447
+ 2. Bump `version` in `pyproject.toml`.
448
+ 3. Publish a GitHub Release with tag `vX.Y.Z` pointing at `main` HEAD.
449
+ 4. `.github/workflows/publish-pypi.yml` checks the tag matches `main`, builds, and
450
+ publishes to PyPI through Trusted Publishing. No tokens stored anywhere.
451
+
452
+ For previews that others need to install before there is a release, use a
453
+ `X.Y.Z.devN` version:
454
+
455
+ - From GitHub: run the `Publish Preview Package` workflow, enter the version, and
456
+ pick `testpypi`. It patches the version inside the CI run only, so no commit.
457
+ - From your machine: `bash ./scripts/publish-local.sh 3.9.2.dev1 testpypi`, with a
458
+ TestPyPI token exported as `TWINE_PASSWORD`.
459
+
460
+ Then, in the consuming project:
461
+
462
+ ```bash
463
+ pip install --index-url https://test.pypi.org/simple/ \
464
+ --extra-index-url https://pypi.org/simple persistence-kit==3.9.2.dev1
465
+ ```
466
+
467
+ Neither PyPI nor TestPyPI lets you re-upload a version, so bump the `devN` on
468
+ every iteration.
469
+
470
+ ## Further reading
471
+
472
+ - [`docs/repositories_and_relations.md`](docs/repositories_and_relations.md): relations, pivots, and how the view repository populates them.
473
+ - [`docs/restclient_and_cache.md`](docs/restclient_and_cache.md): the REST client, its auth strategies, and the cache.
474
+
475
+ Author: Andres Felipe Serrano Barrios · [github.com/AndresFSerrano/persistence-kit](https://github.com/AndresFSerrano/persistence-kit)
476
+