persistence-kit 3.9.0__tar.gz → 3.10.1__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.
- persistence_kit-3.10.1/PKG-INFO +476 -0
- persistence_kit-3.10.1/README.md +431 -0
- persistence_kit-3.10.1/persistence_kit/cache/dynamodb.py +97 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/cache/factory.py +17 -1
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +31 -12
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/factory.py +11 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/providers/cognito_identity_provider.py +203 -54
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/settings/app_settings.py +3 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/settings/cache_settings.py +1 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/pyproject.toml +1 -1
- persistence_kit-3.9.0/PKG-INFO +0 -401
- persistence_kit-3.9.0/README.md +0 -356
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/LICENSE +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/api/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/api/common.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/api/error_handlers.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/api/exceptions.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/api/rate_limit.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/api/route_loader.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/authenticated_user.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/bootstrap/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/bootstrap/configuration.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/bootstrap/seeders.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/bootstrap/startup.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/cache/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/cache/contracts.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/cache/memory.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/cache/mongo.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/cache/namespaced.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/contracts/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/contracts/repository.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/contracts/view_repository.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/py.typed +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/filter_ops.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/memory_repo/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/mongo_repo/mongo_repo.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/factory/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/factory/repository_factory.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/registry/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/view/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/resilience/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/resilience/circuit.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/aggregate.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/api_key.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/base.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/basic.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/bearer.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/login.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/auth/oauth2.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/caching.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/client.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/config.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/contracts.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/errors.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/factory.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/mapping.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/memory.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/payload.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/populate.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/provider.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/registry.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/resolver.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/restclient/retry.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/ports.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/providers/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/providers/memory_security_provider.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/registration.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/token_verifiers/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/token_verifiers/cognito_jwt_verifier.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/security/token_verifiers/memory_jwt_verifier.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/settings/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/settings/constants.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/settings/parsers.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/settings/repo_settings.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/contracts.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/errors.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/factory.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/local.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/media.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/routes.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/storage/s3.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/utils/__init__.py +0 -0
- {persistence_kit-3.9.0 → persistence_kit-3.10.1}/persistence_kit/utils/upsert.py +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: persistence-kit
|
|
3
|
+
Version: 3.10.1
|
|
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
|
+
|