persistence-kit 3.14.1__tar.gz → 3.15.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.15.0}/PKG-INFO +13 -2
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/README.md +10 -1
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/rate_limit.py +6 -11
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/client.py +12 -4
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/factory.py +2 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/memory.py +21 -16
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/registry.py +3 -0
- persistence_kit-3.15.0/persistence_kit/restclient/tracing.py +59 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/app_settings.py +2 -1
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/pyproject.toml +6 -3
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/LICENSE +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/common.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/encrypted_middleware.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/encrypted_routes.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/error_handlers.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/exceptions.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/api/route_loader.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/authenticated_user.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/bootstrap/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/bootstrap/configuration.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/bootstrap/seeders.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/bootstrap/startup.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/dynamodb.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/memory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/mongo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/cache/namespaced.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/contracts/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/contracts/repository.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/contracts/view_repository.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/mailer.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/memory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/message.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/mime.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/preview.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/smtp.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/mail/templates.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/py.typed +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/filter_ops.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/memory_repo/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/mongo_repo/mongo_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/factory/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/factory/repository_factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/registry/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/view/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/resilience/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/resilience/circuit.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/aggregate.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/api_key.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/base.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/basic.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/bearer.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/login.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/auth/oauth2.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/caching.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/config.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/mapping.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/payload.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/populate.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/resolver.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/restclient/retry.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/encrypted/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/encrypted/envelope.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/encrypted/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/ports.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/providers/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/providers/cognito_identity_provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/providers/encryption_provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/providers/memory_security_provider.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/registration.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/token_verifiers/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/token_verifiers/cognito_jwt_verifier.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/token_verifiers/memory_jwt_verifier.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/cache_settings.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/constants.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/mail_settings.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/parsers.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/settings/repo_settings.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/contracts.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/errors.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/factory.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/local.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/media.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/routes.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/storage/s3.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/utils/__init__.py +0 -0
- {persistence_kit-3.14.1 → persistence_kit-3.15.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.15.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
|
|
@@ -329,6 +332,13 @@ real one in tests.
|
|
|
329
332
|
Base URLs can be overridden per environment with `REST_SERVICE_URLS`, a JSON map
|
|
330
333
|
of service name to URL, without touching the registration.
|
|
331
334
|
|
|
335
|
+
With `[tracing]` installed and a tracer provider in the app, every call is a client
|
|
336
|
+
span inside the current trace, named `<METHOD> <path>` (`GET /api/invoices`) and
|
|
337
|
+
tagged with the service in `peer.service`, with the host, the status and the retries. The query string is
|
|
338
|
+
never recorded, and a failure keeps only the error type, not its message, so a
|
|
339
|
+
password sent as a query parameter does not reach the traces. `MemoryRestClient`
|
|
340
|
+
registered under a name produces the same spans, so they show up in local runs too.
|
|
341
|
+
|
|
332
342
|
The cache is a plain key-value store with TTL, chosen by `CACHE_BACKEND` the
|
|
333
343
|
same way repositories are chosen by `REPO_DATABASE`. Set `CACHE_NAMESPACE` when
|
|
334
344
|
several apps share one DynamoDB table or Mongo collection so their keys do not
|
|
@@ -449,6 +459,7 @@ The factories read that object: `get_identity_provider`, `get_token_verifier`,
|
|
|
449
459
|
| `CACHE_BACKEND` | `memory`, `mongo` or `dynamodb`. Defaults to `memory` |
|
|
450
460
|
| `CACHE_NAMESPACE` | Key prefix, so apps sharing a cache store do not collide |
|
|
451
461
|
| `REST_SERVICE_URLS` | JSON map `{"service": "base_url"}` overriding registered REST base URLs |
|
|
462
|
+
| `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
463
|
| `MONGO_DSN`, `MONGO_DB` | Mongo connection |
|
|
453
464
|
| `POSTGRES_DSN` | Full DSN, if you prefer it to the pieces below |
|
|
454
465
|
| `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
|
|
@@ -280,6 +281,13 @@ real one in tests.
|
|
|
280
281
|
Base URLs can be overridden per environment with `REST_SERVICE_URLS`, a JSON map
|
|
281
282
|
of service name to URL, without touching the registration.
|
|
282
283
|
|
|
284
|
+
With `[tracing]` installed and a tracer provider in the app, every call is a client
|
|
285
|
+
span inside the current trace, named `<METHOD> <path>` (`GET /api/invoices`) and
|
|
286
|
+
tagged with the service in `peer.service`, with the host, the status and the retries. The query string is
|
|
287
|
+
never recorded, and a failure keeps only the error type, not its message, so a
|
|
288
|
+
password sent as a query parameter does not reach the traces. `MemoryRestClient`
|
|
289
|
+
registered under a name produces the same spans, so they show up in local runs too.
|
|
290
|
+
|
|
283
291
|
The cache is a plain key-value store with TTL, chosen by `CACHE_BACKEND` the
|
|
284
292
|
same way repositories are chosen by `REPO_DATABASE`. Set `CACHE_NAMESPACE` when
|
|
285
293
|
several apps share one DynamoDB table or Mongo collection so their keys do not
|
|
@@ -400,6 +408,7 @@ The factories read that object: `get_identity_provider`, `get_token_verifier`,
|
|
|
400
408
|
| `CACHE_BACKEND` | `memory`, `mongo` or `dynamodb`. Defaults to `memory` |
|
|
401
409
|
| `CACHE_NAMESPACE` | Key prefix, so apps sharing a cache store do not collide |
|
|
402
410
|
| `REST_SERVICE_URLS` | JSON map `{"service": "base_url"}` overriding registered REST base URLs |
|
|
411
|
+
| `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
412
|
| `MONGO_DSN`, `MONGO_DB` | Mongo connection |
|
|
404
413
|
| `POSTGRES_DSN` | Full DSN, if you prefer it to the pieces below |
|
|
405
414
|
| `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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "persistence-kit"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.15.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.15.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.15.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.15.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.15.0}/persistence_kit/repository/memory_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.15.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.15.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.15.0}/persistence_kit/restclient/auth/__init__.py
RENAMED
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.15.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.15.0}/persistence_kit/security/encrypted/__init__.py
RENAMED
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.15.0}/persistence_kit/security/encrypted/envelope.py
RENAMED
|
File without changes
|
{persistence_kit-3.14.1 → persistence_kit-3.15.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.15.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.15.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
|
|
File without changes
|
|
File without changes
|