persistence-kit 3.14.1__tar.gz → 3.16.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.
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/PKG-INFO +22 -2
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/README.md +19 -1
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/rate_limit.py +6 -11
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/client.py +12 -4
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/factory.py +2 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/memory.py +21 -16
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/registry.py +3 -0
- persistence_kit-3.16.0/persistence_kit/restclient/tracing.py +59 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/app_settings.py +2 -1
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/factory.py +3 -3
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/s3.py +32 -16
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/pyproject.toml +6 -3
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/LICENSE +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/common.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/encrypted_middleware.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/encrypted_routes.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/error_handlers.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/exceptions.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/route_loader.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/authenticated_user.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/bootstrap/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/bootstrap/configuration.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/bootstrap/seeders.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/bootstrap/startup.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/dynamodb.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/memory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/mongo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/cache/namespaced.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/contracts/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/contracts/repository.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/contracts/view_repository.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/mailer.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/memory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/message.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/mime.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/preview.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/smtp.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/mail/templates.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/py.typed +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/filter_ops.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/memory_repo/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/mongo_repo/mongo_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/factory/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/factory/repository_factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/registry/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/view/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/resilience/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/resilience/circuit.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/aggregate.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/api_key.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/base.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/basic.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/bearer.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/login.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/oauth2.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/caching.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/config.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/mapping.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/payload.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/populate.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/resolver.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/retry.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/encrypted/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/encrypted/envelope.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/encrypted/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/ports.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/providers/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/providers/cognito_identity_provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/providers/encryption_provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/providers/memory_security_provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/registration.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/token_verifiers/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/token_verifiers/cognito_jwt_verifier.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/token_verifiers/memory_jwt_verifier.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/cache_settings.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/constants.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/mail_settings.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/parsers.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/repo_settings.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/local.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/media.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/storage/routes.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/utils/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.16.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: 3.
|
|
3
|
+
Version: 3.16.0
|
|
4
4
|
Summary: Reusable persistence and repository toolkit
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -30,6 +30,7 @@ Provides-Extra: security-cognito
|
|
|
30
30
|
Provides-Extra: storage-routes
|
|
31
31
|
Provides-Extra: storage-s3
|
|
32
32
|
Provides-Extra: testing
|
|
33
|
+
Provides-Extra: tracing
|
|
33
34
|
Requires-Dist: asyncpg (>=0.30.0,<1.0.0)
|
|
34
35
|
Requires-Dist: boto3 (>=1.35.0,<2.0.0) ; extra == "storage-s3" or extra == "security-cognito" or extra == "dynamodb" or extra == "all"
|
|
35
36
|
Requires-Dist: cryptography (>=42.0.0) ; extra == "encrypted" or extra == "testing" or extra == "all"
|
|
@@ -37,6 +38,7 @@ Requires-Dist: fastapi (>=0.115.0,<1.0.0) ; extra == "api" or extra == "storage-
|
|
|
37
38
|
Requires-Dist: httpx (>=0.28.0,<1.0.0) ; extra == "restclient" or extra == "testing" or extra == "all"
|
|
38
39
|
Requires-Dist: jinja2 (>=3.1.4,<4.0.0) ; extra == "mail" or extra == "testing" or extra == "all"
|
|
39
40
|
Requires-Dist: motor (>=3.7.1,<4.0.0)
|
|
41
|
+
Requires-Dist: opentelemetry-api (>=1.20.0,<2.0.0) ; extra == "tracing" or extra == "testing" or extra == "all"
|
|
40
42
|
Requires-Dist: pydantic-settings (>=2.3.0,<3.0.0)
|
|
41
43
|
Requires-Dist: pyjwt[crypto] (>=2.10.1,<3.0.0) ; extra == "security" or extra == "security-cognito" or extra == "testing" or extra == "all"
|
|
42
44
|
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"
|
|
@@ -156,7 +158,8 @@ pip install "persistence-kit[api,security,restclient]"
|
|
|
156
158
|
| `restclient` | The REST client and its cache | httpx |
|
|
157
159
|
| `encrypted` | Encrypted request and response payloads | fastapi, cryptography |
|
|
158
160
|
| `mail` | Mail templates rendered with Jinja2 (sending itself needs no extra) | jinja2 |
|
|
159
|
-
| `
|
|
161
|
+
| `tracing` | A span for every REST call, inside the request's trace | opentelemetry-api |
|
|
162
|
+
| `testing` | Everything the test suite needs | fastapi, pyjwt, httpx, python-multipart, cryptography, jinja2, opentelemetry-api |
|
|
160
163
|
| `all` | Every optional capability | all of the above |
|
|
161
164
|
|
|
162
165
|
Importing `persistence_kit` never loads an optional dependency by itself. Ask for
|
|
@@ -206,6 +209,15 @@ url = await storage.generate_presigned_url(key)
|
|
|
206
209
|
`LocalObjectStorage` writes to a directory and signs its own download URLs.
|
|
207
210
|
`S3ObjectStorage` uploads to S3 and returns AWS presigned URLs. Pick one with
|
|
208
211
|
`get_export_storage(settings)`, which reads your settings and caches the adapter.
|
|
212
|
+
Give `S3ObjectStorage` a `prefix` and every key it writes, presigns, deletes or
|
|
213
|
+
lists stays under that folder of the bucket, while the keys you pass and get back
|
|
214
|
+
stay relative to it.
|
|
215
|
+
|
|
216
|
+
For files your users upload, `build_media_storage(settings, namespace=...,
|
|
217
|
+
url_prefix=...)` checks type and size and keeps each namespace in its own
|
|
218
|
+
folder: `<LOCAL_MEDIA_STORAGE_DIR>/<namespace>` with `MEDIA_STORAGE_PROVIDER=local`,
|
|
219
|
+
and `<namespace>/` inside `AWS_S3_MEDIA_BUCKET` with `s3`. Keys are the same on
|
|
220
|
+
both, so moving to S3 does not change what your app stores.
|
|
209
221
|
|
|
210
222
|
FastAPI apps can mount `build_local_export_storage_router(...)` to serve local
|
|
211
223
|
files, passing their settings provider, an optional current-user dependency, and
|
|
@@ -329,6 +341,13 @@ real one in tests.
|
|
|
329
341
|
Base URLs can be overridden per environment with `REST_SERVICE_URLS`, a JSON map
|
|
330
342
|
of service name to URL, without touching the registration.
|
|
331
343
|
|
|
344
|
+
With `[tracing]` installed and a tracer provider in the app, every call is a client
|
|
345
|
+
span inside the current trace, named `<METHOD> <path>` (`GET /api/invoices`) and
|
|
346
|
+
tagged with the service in `peer.service`, with the host, the status and the retries. The query string is
|
|
347
|
+
never recorded, and a failure keeps only the error type, not its message, so a
|
|
348
|
+
password sent as a query parameter does not reach the traces. `MemoryRestClient`
|
|
349
|
+
registered under a name produces the same spans, so they show up in local runs too.
|
|
350
|
+
|
|
332
351
|
The cache is a plain key-value store with TTL, chosen by `CACHE_BACKEND` the
|
|
333
352
|
same way repositories are chosen by `REPO_DATABASE`. Set `CACHE_NAMESPACE` when
|
|
334
353
|
several apps share one DynamoDB table or Mongo collection so their keys do not
|
|
@@ -449,6 +468,7 @@ The factories read that object: `get_identity_provider`, `get_token_verifier`,
|
|
|
449
468
|
| `CACHE_BACKEND` | `memory`, `mongo` or `dynamodb`. Defaults to `memory` |
|
|
450
469
|
| `CACHE_NAMESPACE` | Key prefix, so apps sharing a cache store do not collide |
|
|
451
470
|
| `REST_SERVICE_URLS` | JSON map `{"service": "base_url"}` overriding registered REST base URLs |
|
|
471
|
+
| `TRUSTED_PROXY_HOPS` | Proxies in front of the app that append to `X-Forwarded-For`. The auth rate limit counts the address the outermost one saw, so a forged header does not open a new bucket. `1` by default (one load balancer), `2` behind CloudFront and an ALB, `0` with no proxy |
|
|
452
472
|
| `MONGO_DSN`, `MONGO_DB` | Mongo connection |
|
|
453
473
|
| `POSTGRES_DSN` | Full DSN, if you prefer it to the pieces below |
|
|
454
474
|
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB` | Postgres connection |
|
|
@@ -107,7 +107,8 @@ pip install "persistence-kit[api,security,restclient]"
|
|
|
107
107
|
| `restclient` | The REST client and its cache | httpx |
|
|
108
108
|
| `encrypted` | Encrypted request and response payloads | fastapi, cryptography |
|
|
109
109
|
| `mail` | Mail templates rendered with Jinja2 (sending itself needs no extra) | jinja2 |
|
|
110
|
-
| `
|
|
110
|
+
| `tracing` | A span for every REST call, inside the request's trace | opentelemetry-api |
|
|
111
|
+
| `testing` | Everything the test suite needs | fastapi, pyjwt, httpx, python-multipart, cryptography, jinja2, opentelemetry-api |
|
|
111
112
|
| `all` | Every optional capability | all of the above |
|
|
112
113
|
|
|
113
114
|
Importing `persistence_kit` never loads an optional dependency by itself. Ask for
|
|
@@ -157,6 +158,15 @@ url = await storage.generate_presigned_url(key)
|
|
|
157
158
|
`LocalObjectStorage` writes to a directory and signs its own download URLs.
|
|
158
159
|
`S3ObjectStorage` uploads to S3 and returns AWS presigned URLs. Pick one with
|
|
159
160
|
`get_export_storage(settings)`, which reads your settings and caches the adapter.
|
|
161
|
+
Give `S3ObjectStorage` a `prefix` and every key it writes, presigns, deletes or
|
|
162
|
+
lists stays under that folder of the bucket, while the keys you pass and get back
|
|
163
|
+
stay relative to it.
|
|
164
|
+
|
|
165
|
+
For files your users upload, `build_media_storage(settings, namespace=...,
|
|
166
|
+
url_prefix=...)` checks type and size and keeps each namespace in its own
|
|
167
|
+
folder: `<LOCAL_MEDIA_STORAGE_DIR>/<namespace>` with `MEDIA_STORAGE_PROVIDER=local`,
|
|
168
|
+
and `<namespace>/` inside `AWS_S3_MEDIA_BUCKET` with `s3`. Keys are the same on
|
|
169
|
+
both, so moving to S3 does not change what your app stores.
|
|
160
170
|
|
|
161
171
|
FastAPI apps can mount `build_local_export_storage_router(...)` to serve local
|
|
162
172
|
files, passing their settings provider, an optional current-user dependency, and
|
|
@@ -280,6 +290,13 @@ real one in tests.
|
|
|
280
290
|
Base URLs can be overridden per environment with `REST_SERVICE_URLS`, a JSON map
|
|
281
291
|
of service name to URL, without touching the registration.
|
|
282
292
|
|
|
293
|
+
With `[tracing]` installed and a tracer provider in the app, every call is a client
|
|
294
|
+
span inside the current trace, named `<METHOD> <path>` (`GET /api/invoices`) and
|
|
295
|
+
tagged with the service in `peer.service`, with the host, the status and the retries. The query string is
|
|
296
|
+
never recorded, and a failure keeps only the error type, not its message, so a
|
|
297
|
+
password sent as a query parameter does not reach the traces. `MemoryRestClient`
|
|
298
|
+
registered under a name produces the same spans, so they show up in local runs too.
|
|
299
|
+
|
|
283
300
|
The cache is a plain key-value store with TTL, chosen by `CACHE_BACKEND` the
|
|
284
301
|
same way repositories are chosen by `REPO_DATABASE`. Set `CACHE_NAMESPACE` when
|
|
285
302
|
several apps share one DynamoDB table or Mongo collection so their keys do not
|
|
@@ -400,6 +417,7 @@ The factories read that object: `get_identity_provider`, `get_token_verifier`,
|
|
|
400
417
|
| `CACHE_BACKEND` | `memory`, `mongo` or `dynamodb`. Defaults to `memory` |
|
|
401
418
|
| `CACHE_NAMESPACE` | Key prefix, so apps sharing a cache store do not collide |
|
|
402
419
|
| `REST_SERVICE_URLS` | JSON map `{"service": "base_url"}` overriding registered REST base URLs |
|
|
420
|
+
| `TRUSTED_PROXY_HOPS` | Proxies in front of the app that append to `X-Forwarded-For`. The auth rate limit counts the address the outermost one saw, so a forged header does not open a new bucket. `1` by default (one load balancer), `2` behind CloudFront and an ALB, `0` with no proxy |
|
|
403
421
|
| `MONGO_DSN`, `MONGO_DB` | Mongo connection |
|
|
404
422
|
| `POSTGRES_DSN` | Full DSN, if you prefer it to the pieces below |
|
|
405
423
|
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB` | Postgres connection |
|
|
@@ -48,16 +48,11 @@ class InMemoryRateLimiter:
|
|
|
48
48
|
return None
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def _resolve_client_id(request: Request) -> str:
|
|
52
|
-
forwarded_for = request.headers.get("x-forwarded-for")
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return first_ip
|
|
57
|
-
|
|
58
|
-
cf_connecting_ip = request.headers.get("cf-connecting-ip")
|
|
59
|
-
if cf_connecting_ip:
|
|
60
|
-
return cf_connecting_ip.strip()
|
|
51
|
+
def _resolve_client_id(request: Request, trusted_proxy_hops: int) -> str:
|
|
52
|
+
forwarded_for = request.headers.get("x-forwarded-for", "")
|
|
53
|
+
chain = [hop.strip() for hop in forwarded_for.split(",") if hop.strip()]
|
|
54
|
+
if 0 < trusted_proxy_hops <= len(chain):
|
|
55
|
+
return chain[-trusted_proxy_hops]
|
|
61
56
|
|
|
62
57
|
client_host = request.client.host if request.client else None
|
|
63
58
|
return client_host or "unknown"
|
|
@@ -87,7 +82,7 @@ def build_auth_rate_limit_dependency(
|
|
|
87
82
|
limit = getattr(settings, limit_setting_name)
|
|
88
83
|
retry_after = await _get_rate_limiter(request).hit(
|
|
89
84
|
scope=scope,
|
|
90
|
-
client_id=_resolve_client_id(request),
|
|
85
|
+
client_id=_resolve_client_id(request, getattr(settings, "trusted_proxy_hops", 1)),
|
|
91
86
|
limit=limit,
|
|
92
87
|
window_seconds=settings.auth_rate_limit_window_seconds,
|
|
93
88
|
)
|
|
@@ -23,6 +23,7 @@ from persistence_kit.restclient.mapping import ModelMappingMixin
|
|
|
23
23
|
from persistence_kit.restclient.payload import prepare_request
|
|
24
24
|
from persistence_kit.restclient.resolver import StaticEndpointResolver
|
|
25
25
|
from persistence_kit.restclient.retry import RetryPolicy
|
|
26
|
+
from persistence_kit.restclient.tracing import traced_call
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class HttpxRestClient(ModelMappingMixin):
|
|
@@ -41,7 +42,9 @@ class HttpxRestClient(ModelMappingMixin):
|
|
|
41
42
|
retry_policy: RetryPolicy | None = None,
|
|
42
43
|
circuit_breaker: CircuitBreaker | None = None,
|
|
43
44
|
client: httpx.AsyncClient | None = None,
|
|
45
|
+
name: str = "",
|
|
44
46
|
) -> None:
|
|
47
|
+
self.name = name
|
|
45
48
|
self._config = config or ServiceConfig()
|
|
46
49
|
self._resolver = resolver or StaticEndpointResolver()
|
|
47
50
|
self._auth = authenticator or NoAuth()
|
|
@@ -89,6 +92,14 @@ class HttpxRestClient(ModelMappingMixin):
|
|
|
89
92
|
)
|
|
90
93
|
request = await self._auth.apply(request)
|
|
91
94
|
|
|
95
|
+
with traced_call(self.name, request.method, request.url) as call:
|
|
96
|
+
response, attempts = await self._exchange(request)
|
|
97
|
+
call.record(response.status_code, attempts)
|
|
98
|
+
if self._config.raise_for_status and not response.is_success:
|
|
99
|
+
raise RestHTTPError(response.status_code, response=response)
|
|
100
|
+
return response
|
|
101
|
+
|
|
102
|
+
async def _exchange(self, request: RestRequest) -> tuple[RestResponse, int]:
|
|
92
103
|
attempt = 0
|
|
93
104
|
refreshed = False
|
|
94
105
|
while True:
|
|
@@ -128,10 +139,7 @@ class HttpxRestClient(ModelMappingMixin):
|
|
|
128
139
|
attempt += 1
|
|
129
140
|
continue
|
|
130
141
|
break
|
|
131
|
-
|
|
132
|
-
if self._config.raise_for_status and not response.is_success:
|
|
133
|
-
raise RestHTTPError(response.status_code, response=response)
|
|
134
|
-
return response
|
|
142
|
+
return response, attempt
|
|
135
143
|
|
|
136
144
|
async def _send(self, request: RestRequest) -> RestResponse:
|
|
137
145
|
try:
|
|
@@ -24,6 +24,7 @@ def build_rest_client(
|
|
|
24
24
|
retry_policy: RetryPolicy | None = None,
|
|
25
25
|
circuit_breaker: CircuitBreaker | None = None,
|
|
26
26
|
settings: Any | None = None,
|
|
27
|
+
name: str = "",
|
|
27
28
|
) -> RestClient:
|
|
28
29
|
if config is None:
|
|
29
30
|
config = (
|
|
@@ -48,4 +49,5 @@ def build_rest_client(
|
|
|
48
49
|
authenticator=authenticator,
|
|
49
50
|
retry_policy=retry_policy,
|
|
50
51
|
circuit_breaker=circuit_breaker,
|
|
52
|
+
name=name,
|
|
51
53
|
)
|
|
@@ -13,6 +13,7 @@ from persistence_kit.restclient.contracts import (
|
|
|
13
13
|
from persistence_kit.restclient.errors import RestHTTPError
|
|
14
14
|
from persistence_kit.restclient.mapping import ModelMappingMixin
|
|
15
15
|
from persistence_kit.restclient.payload import prepare_request
|
|
16
|
+
from persistence_kit.restclient.tracing import traced_call
|
|
16
17
|
|
|
17
18
|
Handler = Callable[[RestRequest], RestResponse]
|
|
18
19
|
|
|
@@ -39,7 +40,9 @@ class MemoryRestClient(ModelMappingMixin):
|
|
|
39
40
|
resolver: EndpointResolver | None = None,
|
|
40
41
|
default_headers: Mapping[str, str] | None = None,
|
|
41
42
|
raise_for_status: bool = True,
|
|
43
|
+
name: str = "",
|
|
42
44
|
) -> None:
|
|
45
|
+
self.name = name
|
|
43
46
|
self._auth = authenticator or NoAuth()
|
|
44
47
|
self._resolver = resolver
|
|
45
48
|
self._default_headers = dict(default_headers or {})
|
|
@@ -100,22 +103,24 @@ class MemoryRestClient(ModelMappingMixin):
|
|
|
100
103
|
request = await self._auth.apply(request)
|
|
101
104
|
self.calls.append(request)
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
with traced_call(self.name, request.method, request.url) as call:
|
|
107
|
+
stub = self._stubs.get((method.upper(), service))
|
|
108
|
+
if stub is None:
|
|
109
|
+
response = RestResponse(
|
|
110
|
+
status_code=404,
|
|
111
|
+
headers={},
|
|
112
|
+
content=b"",
|
|
113
|
+
url=url,
|
|
114
|
+
)
|
|
115
|
+
elif callable(stub):
|
|
116
|
+
response = stub(request)
|
|
117
|
+
else:
|
|
118
|
+
response = stub
|
|
119
|
+
|
|
120
|
+
call.record(response.status_code)
|
|
121
|
+
if self._raise and not response.is_success:
|
|
122
|
+
raise RestHTTPError(response.status_code, response=response)
|
|
123
|
+
return response
|
|
119
124
|
|
|
120
125
|
async def get(self, service: str, **kwargs: Any) -> RestResponse:
|
|
121
126
|
return await self.request("GET", service, **kwargs)
|
|
@@ -83,6 +83,8 @@ class RestClientRegistry:
|
|
|
83
83
|
Inyecta dobles de prueba o stubs sin pasar por el transporte HTTP: ``get``
|
|
84
84
|
devuelve este cliente tal cual, sin construir un ``HttpxRestClient``.
|
|
85
85
|
"""
|
|
86
|
+
if hasattr(client, "name") and not client.name:
|
|
87
|
+
client.name = name
|
|
86
88
|
self._clients[name] = client
|
|
87
89
|
return self
|
|
88
90
|
|
|
@@ -108,6 +110,7 @@ class RestClientRegistry:
|
|
|
108
110
|
authenticator=service.authenticator,
|
|
109
111
|
retry_policy=service.retry_policy,
|
|
110
112
|
circuit_breaker=service.circuit_breaker,
|
|
113
|
+
name=name,
|
|
111
114
|
)
|
|
112
115
|
|
|
113
116
|
if service.config.cache_ttl_seconds or service.config.cacheable:
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Any, Iterator
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from opentelemetry.trace import SpanKind, Status, StatusCode
|
|
10
|
+
except ImportError:
|
|
11
|
+
trace = None
|
|
12
|
+
|
|
13
|
+
TRACER_NAME = "persistence_kit.restclient"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TracedCall:
|
|
17
|
+
def __init__(self, span: Any = None) -> None:
|
|
18
|
+
self._span = span
|
|
19
|
+
|
|
20
|
+
def record(self, status_code: int, attempts: int = 0) -> None:
|
|
21
|
+
if self._span is None:
|
|
22
|
+
return
|
|
23
|
+
self._span.set_attribute("http.response.status_code", status_code)
|
|
24
|
+
if attempts:
|
|
25
|
+
self._span.set_attribute("http.request.resend_count", attempts)
|
|
26
|
+
if status_code >= 400:
|
|
27
|
+
self._span.set_status(Status(StatusCode.ERROR))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def traced_call(service_name: str, method: str, url: str) -> Iterator[TracedCall]:
|
|
32
|
+
if trace is None:
|
|
33
|
+
yield TracedCall()
|
|
34
|
+
return
|
|
35
|
+
parts = urlsplit(url)
|
|
36
|
+
path = parts.path or "/"
|
|
37
|
+
verb = method.upper()
|
|
38
|
+
tracer = trace.get_tracer(TRACER_NAME)
|
|
39
|
+
with tracer.start_as_current_span(
|
|
40
|
+
f"{verb} {path}",
|
|
41
|
+
kind=SpanKind.CLIENT,
|
|
42
|
+
record_exception=False,
|
|
43
|
+
set_status_on_exception=False,
|
|
44
|
+
) as span:
|
|
45
|
+
span.set_attribute("http.request.method", verb)
|
|
46
|
+
span.set_attribute("url.path", path)
|
|
47
|
+
if parts.hostname:
|
|
48
|
+
span.set_attribute("server.address", parts.hostname)
|
|
49
|
+
if service_name:
|
|
50
|
+
span.set_attribute("peer.service", service_name)
|
|
51
|
+
try:
|
|
52
|
+
yield TracedCall(span)
|
|
53
|
+
except Exception as error:
|
|
54
|
+
status_code = getattr(error, "status_code", None)
|
|
55
|
+
if isinstance(status_code, int):
|
|
56
|
+
span.set_attribute("http.response.status_code", status_code)
|
|
57
|
+
span.set_attribute("error.type", type(error).__name__)
|
|
58
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
59
|
+
raise
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
-
from pydantic import field_validator, model_validator
|
|
3
|
+
from pydantic import Field, field_validator, model_validator
|
|
4
4
|
from pydantic_settings import SettingsConfigDict
|
|
5
5
|
|
|
6
6
|
from persistence_kit.settings.repo_settings import RepoSettings
|
|
@@ -57,6 +57,7 @@ class PersistenceKitSettings(RepoSettings):
|
|
|
57
57
|
auth_login_rate_limit: int = 10
|
|
58
58
|
auth_refresh_rate_limit: int = 30
|
|
59
59
|
auth_password_rate_limit: int = 5
|
|
60
|
+
trusted_proxy_hops: int = Field(default=1, ge=0)
|
|
60
61
|
|
|
61
62
|
# Export Storage
|
|
62
63
|
export_storage_provider: ExportStorageProvider = ExportStorageProvider.LOCAL
|
|
@@ -87,10 +87,10 @@ def _local_media_object_storage_cached(base_dir: str) -> ObjectStorage:
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
@lru_cache
|
|
90
|
-
def _s3_media_object_storage_cached(bucket: str, region: str) -> ObjectStorage:
|
|
90
|
+
def _s3_media_object_storage_cached(bucket: str, region: str, prefix: str) -> ObjectStorage:
|
|
91
91
|
from persistence_kit.storage.s3 import S3ObjectStorage
|
|
92
92
|
|
|
93
|
-
return S3ObjectStorage(bucket=bucket, region=region)
|
|
93
|
+
return S3ObjectStorage(bucket=bucket, region=region, prefix=prefix)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def _build_media_object_storage(
|
|
@@ -110,7 +110,7 @@ def _build_media_object_storage(
|
|
|
110
110
|
"Falta configuracion AWS_S3_MEDIA_BUCKET para el storage de media."
|
|
111
111
|
)
|
|
112
112
|
return _s3_media_object_storage_cached(
|
|
113
|
-
settings.aws_s3_media_bucket, settings.aws_region
|
|
113
|
+
settings.aws_s3_media_bucket, settings.aws_region, namespace
|
|
114
114
|
)
|
|
115
115
|
provider = getattr(
|
|
116
116
|
settings.media_storage_provider, "value", settings.media_storage_provider
|
|
@@ -22,35 +22,42 @@ logger = logging.getLogger(__name__)
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class S3ObjectStorage:
|
|
25
|
-
def __init__(self, *, bucket: str, region: str) -> None:
|
|
25
|
+
def __init__(self, *, bucket: str, region: str, prefix: str = "") -> None:
|
|
26
26
|
if boto3 is None:
|
|
27
27
|
raise StorageConfigError("Falta dependencia boto3 para integrar con S3.")
|
|
28
28
|
self._bucket = bucket
|
|
29
29
|
self._region = region
|
|
30
|
+
normalized_prefix = prefix.strip("/")
|
|
31
|
+
self._prefix = f"{normalized_prefix}/" if normalized_prefix else ""
|
|
30
32
|
self._client = boto3.client("s3", region_name=region)
|
|
31
33
|
|
|
34
|
+
def _object_key(self, key: str) -> str:
|
|
35
|
+
return f"{self._prefix}{key}"
|
|
36
|
+
|
|
32
37
|
async def upload(
|
|
33
38
|
self,
|
|
34
39
|
key: str,
|
|
35
40
|
data: bytes,
|
|
36
41
|
content_type: str = "application/octet-stream",
|
|
37
42
|
) -> str:
|
|
43
|
+
object_key = self._object_key(key)
|
|
44
|
+
|
|
38
45
|
def _upload() -> None:
|
|
39
46
|
self._client.put_object(
|
|
40
47
|
Bucket=self._bucket,
|
|
41
|
-
Key=
|
|
48
|
+
Key=object_key,
|
|
42
49
|
Body=data,
|
|
43
50
|
ContentType=content_type,
|
|
44
51
|
)
|
|
45
52
|
|
|
46
|
-
logger.info("S3 upload iniciado bucket=%s key=%s", self._bucket,
|
|
53
|
+
logger.info("S3 upload iniciado bucket=%s key=%s", self._bucket, object_key)
|
|
47
54
|
try:
|
|
48
55
|
await asyncio.to_thread(_upload)
|
|
49
56
|
except ClientError as exc:
|
|
50
|
-
logger.exception("S3 upload fallido bucket=%s key=%s", self._bucket,
|
|
57
|
+
logger.exception("S3 upload fallido bucket=%s key=%s", self._bucket, object_key)
|
|
51
58
|
raise StorageUploadError(f"No fue posible subir el archivo a S3: {exc}") from exc
|
|
52
59
|
|
|
53
|
-
logger.info("S3 upload exitoso bucket=%s key=%s", self._bucket,
|
|
60
|
+
logger.info("S3 upload exitoso bucket=%s key=%s", self._bucket, object_key)
|
|
54
61
|
return key
|
|
55
62
|
|
|
56
63
|
async def upload_file(
|
|
@@ -59,66 +66,75 @@ class S3ObjectStorage:
|
|
|
59
66
|
file_path: Path,
|
|
60
67
|
content_type: str = "application/octet-stream",
|
|
61
68
|
) -> str:
|
|
69
|
+
object_key = self._object_key(key)
|
|
70
|
+
|
|
62
71
|
def _upload_file() -> None:
|
|
63
72
|
with file_path.open("rb") as file_obj:
|
|
64
73
|
self._client.upload_fileobj(
|
|
65
74
|
Fileobj=file_obj,
|
|
66
75
|
Bucket=self._bucket,
|
|
67
|
-
Key=
|
|
76
|
+
Key=object_key,
|
|
68
77
|
ExtraArgs={"ContentType": content_type},
|
|
69
78
|
)
|
|
70
79
|
|
|
71
|
-
logger.info(
|
|
80
|
+
logger.info(
|
|
81
|
+
"S3 file upload iniciado bucket=%s key=%s path=%s", self._bucket, object_key, file_path
|
|
82
|
+
)
|
|
72
83
|
try:
|
|
73
84
|
await asyncio.to_thread(_upload_file)
|
|
74
85
|
except ClientError as exc:
|
|
75
|
-
logger.exception("S3 file upload fallido bucket=%s key=%s", self._bucket,
|
|
86
|
+
logger.exception("S3 file upload fallido bucket=%s key=%s", self._bucket, object_key)
|
|
76
87
|
raise StorageUploadError(f"No fue posible subir el archivo a S3: {exc}") from exc
|
|
77
88
|
|
|
78
|
-
logger.info("S3 file upload exitoso bucket=%s key=%s", self._bucket,
|
|
89
|
+
logger.info("S3 file upload exitoso bucket=%s key=%s", self._bucket, object_key)
|
|
79
90
|
return key
|
|
80
91
|
|
|
81
92
|
async def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str:
|
|
93
|
+
object_key = self._object_key(key)
|
|
94
|
+
|
|
82
95
|
def _presign() -> str:
|
|
83
96
|
return self._client.generate_presigned_url(
|
|
84
97
|
"get_object",
|
|
85
|
-
Params={"Bucket": self._bucket, "Key":
|
|
98
|
+
Params={"Bucket": self._bucket, "Key": object_key},
|
|
86
99
|
ExpiresIn=expires_in,
|
|
87
100
|
)
|
|
88
101
|
|
|
89
102
|
try:
|
|
90
103
|
return await asyncio.to_thread(_presign)
|
|
91
104
|
except ClientError as exc:
|
|
92
|
-
logger.exception("S3 presign fallido bucket=%s key=%s", self._bucket,
|
|
105
|
+
logger.exception("S3 presign fallido bucket=%s key=%s", self._bucket, object_key)
|
|
93
106
|
raise StoragePresignError(f"No fue posible generar la URL prefirmada: {exc}") from exc
|
|
94
107
|
|
|
95
108
|
async def delete(self, key: str) -> None:
|
|
109
|
+
object_key = self._object_key(key)
|
|
110
|
+
|
|
96
111
|
def _delete() -> None:
|
|
97
|
-
self._client.delete_object(Bucket=self._bucket, Key=
|
|
112
|
+
self._client.delete_object(Bucket=self._bucket, Key=object_key)
|
|
98
113
|
|
|
99
114
|
try:
|
|
100
115
|
await asyncio.to_thread(_delete)
|
|
101
116
|
except ClientError as exc:
|
|
102
|
-
logger.exception("S3 delete fallido bucket=%s key=%s", self._bucket,
|
|
117
|
+
logger.exception("S3 delete fallido bucket=%s key=%s", self._bucket, object_key)
|
|
103
118
|
raise StorageError(f"No fue posible borrar el objeto en S3: {exc}") from exc
|
|
104
119
|
|
|
105
120
|
async def list_keys(self, prefix: str = "") -> list[str]:
|
|
106
121
|
normalized_prefix = prefix.lstrip("/")
|
|
107
122
|
if normalized_prefix and not normalized_prefix.endswith("/"):
|
|
108
123
|
normalized_prefix = normalized_prefix + "/"
|
|
124
|
+
search_prefix = self._object_key(normalized_prefix)
|
|
109
125
|
|
|
110
126
|
def _list() -> list[str]:
|
|
111
127
|
keys: list[str] = []
|
|
112
128
|
paginator = self._client.get_paginator("list_objects_v2")
|
|
113
|
-
for page in paginator.paginate(Bucket=self._bucket, Prefix=
|
|
129
|
+
for page in paginator.paginate(Bucket=self._bucket, Prefix=search_prefix):
|
|
114
130
|
for obj in page.get("Contents", []) or []:
|
|
115
|
-
keys.append(obj["Key"])
|
|
131
|
+
keys.append(obj["Key"].removeprefix(self._prefix))
|
|
116
132
|
return keys
|
|
117
133
|
|
|
118
134
|
try:
|
|
119
135
|
return await asyncio.to_thread(_list)
|
|
120
136
|
except ClientError as exc:
|
|
121
|
-
logger.exception("S3 list fallido bucket=%s prefix=%s", self._bucket,
|
|
137
|
+
logger.exception("S3 list fallido bucket=%s prefix=%s", self._bucket, search_prefix)
|
|
122
138
|
raise StorageError(f"No fue posible listar objetos en S3: {exc}") from exc
|
|
123
139
|
|
|
124
140
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "persistence-kit"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.16.0"
|
|
4
4
|
description = "Reusable persistence and repository toolkit"
|
|
5
5
|
authors = ["Andres Felipe Serrano Barrios <andresfserrano1@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -37,6 +37,7 @@ python-multipart = {version = ">=0.0.31,<1.0.0", optional = true}
|
|
|
37
37
|
httpx = {version = ">=0.28.0,<1.0.0", optional = true}
|
|
38
38
|
cryptography = {version = ">=42.0.0", optional = true}
|
|
39
39
|
jinja2 = {version = "^3.1.4", optional = true}
|
|
40
|
+
opentelemetry-api = {version = ">=1.20.0,<2.0.0", optional = true}
|
|
40
41
|
|
|
41
42
|
[tool.poetry.extras]
|
|
42
43
|
api = ["fastapi", "python-multipart"]
|
|
@@ -48,12 +49,14 @@ encrypted = ["fastapi", "cryptography"]
|
|
|
48
49
|
dynamodb = ["boto3"]
|
|
49
50
|
restclient = ["httpx"]
|
|
50
51
|
mail = ["jinja2"]
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
tracing = ["opentelemetry-api"]
|
|
53
|
+
testing = ["fastapi", "pyjwt", "python-multipart", "httpx", "cryptography", "jinja2", "opentelemetry-api"]
|
|
54
|
+
all = ["fastapi", "boto3", "pyjwt", "python-multipart", "httpx", "cryptography", "jinja2", "opentelemetry-api"]
|
|
53
55
|
|
|
54
56
|
[tool.poetry.group.dev.dependencies]
|
|
55
57
|
pytest = "^8.3.0"
|
|
56
58
|
pytest-asyncio = ">=0.23.0,<2.0.0"
|
|
59
|
+
opentelemetry-sdk = ">=1.20.0,<2.0.0"
|
|
57
60
|
|
|
58
61
|
[tool.pytest.ini_options]
|
|
59
62
|
testpaths = ["tests"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/api/encrypted_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/bootstrap/configuration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/contracts/view_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/memory_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository/mongo_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/repository_factory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/__init__.py
RENAMED
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/restclient/auth/api_key.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/encrypted/__init__.py
RENAMED
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/encrypted/envelope.py
RENAMED
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/encrypted/errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/security/providers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.16.0}/persistence_kit/settings/cache_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|