omkit 0.0.2__py3-none-any.whl
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.
- omkit/__init__.py +18 -0
- omkit/cleanup.py +62 -0
- omkit/config.py +60 -0
- omkit/cost.py +78 -0
- omkit/data/__init__.py +33 -0
- omkit/dbpool.py +139 -0
- omkit/encryption.py +58 -0
- omkit/eventbus.py +360 -0
- omkit/events.py +23 -0
- omkit/health.py +66 -0
- omkit/http.py +82 -0
- omkit/internal/__init__.py +7 -0
- omkit/internal/crypto.py +17 -0
- omkit/jobqueue/__init__.py +28 -0
- omkit/jobqueue/envelope.py +116 -0
- omkit/jobqueue/streaq.py +267 -0
- omkit/logging.py +77 -0
- omkit/metrics.py +41 -0
- omkit/model_lifecycle.py +192 -0
- omkit/platform/__init__.py +18 -0
- omkit/providers/__init__.py +11 -0
- omkit/providers/base.py +76 -0
- omkit/providers/registry.py +263 -0
- omkit/py.typed +0 -0
- omkit/quota.py +186 -0
- omkit/resilience.py +122 -0
- omkit/sanitize.py +122 -0
- omkit/security/__init__.py +28 -0
- omkit/security/events.py +79 -0
- omkit/sessions.py +301 -0
- omkit/settings.py +348 -0
- omkit/sync_notifier.py +110 -0
- omkit/tenant.py +271 -0
- omkit/tracing.py +80 -0
- omkit/transport/__init__.py +29 -0
- omkit/valkey.py +45 -0
- omkit-0.0.2.dist-info/METADATA +29 -0
- omkit-0.0.2.dist-info/RECORD +40 -0
- omkit-0.0.2.dist-info/WHEEL +5 -0
- omkit-0.0.2.dist-info/top_level.txt +1 -0
omkit/tenant.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""packages/omur-sdk/omkit/tenant.py — Per-request tenant isolation via contextvars.
|
|
2
|
+
|
|
3
|
+
Middleware sets the tenant on each request. Handlers call require() to access.
|
|
4
|
+
Background tasks use bind() to establish context.
|
|
5
|
+
|
|
6
|
+
exports: require() | current_or_none() | request_id() | _DEFAULT_EXCLUDE | class TenantMiddleware | middleware(exclude_paths) | set_rls(session) | set_rls_conn(conn) | bind(tenant_id, request_id) | async_bind(tenant_id, request_id) | hashed_for_log(tenant_id, key)
|
|
7
|
+
rules: The tenant middleware must be applied before any database operations to ensure RLS policies are properly set, and all tenant context must be bound to the current request scope to maintain isolation between concurrent requests.
|
|
8
|
+
agent: ollama/qwen3-coder:latest | ollama | 2026-05-01 | codedna-cli | initial CodeDNA annotation pass
|
|
9
|
+
message:
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import uuid
|
|
19
|
+
from contextvars import ContextVar
|
|
20
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
21
|
+
from typing import TYPE_CHECKING, AsyncIterator, Callable
|
|
22
|
+
|
|
23
|
+
import structlog
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import asyncpg
|
|
27
|
+
|
|
28
|
+
log = structlog.get_logger()
|
|
29
|
+
|
|
30
|
+
_tenant_id_var: ContextVar[str | None] = ContextVar("tenant_id", default=None)
|
|
31
|
+
_request_id_var: ContextVar[str | None] = ContextVar("request_id", default=None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def require() -> str:
|
|
35
|
+
"""Return current tenant ID or raise RuntimeError.
|
|
36
|
+
|
|
37
|
+
Rules: Tenant context must be set before calling this function, otherwise a RuntimeError is raised. Used in FastAPI middleware or background tasks via bind().
|
|
38
|
+
"""
|
|
39
|
+
tid = _tenant_id_var.get()
|
|
40
|
+
if tid is None:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"No tenant context set. Use tenant.middleware() in FastAPI "
|
|
43
|
+
"or tenant.bind() for background tasks."
|
|
44
|
+
)
|
|
45
|
+
return tid
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def current_or_none() -> str | None:
|
|
49
|
+
"""Return current tenant ID or None. For shared services where tenant is optional.
|
|
50
|
+
|
|
51
|
+
Rules: Returns None if no tenant context is set; intended for optional tenant scenarios like shared services.
|
|
52
|
+
"""
|
|
53
|
+
return _tenant_id_var.get()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def request_id() -> str | None:
|
|
57
|
+
"""Return current request ID or None.
|
|
58
|
+
|
|
59
|
+
Rules: Returns None if no request ID is set; used for tracking requests across services.
|
|
60
|
+
"""
|
|
61
|
+
return _request_id_var.get()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_DEFAULT_EXCLUDE = frozenset({"/health", "/healthz", "/ready", "/readyz", "/metrics"})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_header(headers: list[tuple[bytes, bytes]], name: bytes) -> str | None:
|
|
68
|
+
"""Extract a header value from raw ASGI headers."""
|
|
69
|
+
for key, value in headers:
|
|
70
|
+
if key.lower() == name:
|
|
71
|
+
return value.decode("latin-1")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_uuid(value: str) -> bool:
|
|
76
|
+
"""Check if value is a valid UUID (any version, case-insensitive)."""
|
|
77
|
+
try:
|
|
78
|
+
uuid.UUID(value)
|
|
79
|
+
return True
|
|
80
|
+
except ValueError:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TenantMiddleware:
|
|
85
|
+
"""Pure ASGI middleware for tenant extraction.
|
|
86
|
+
|
|
87
|
+
Usage with FastAPI:
|
|
88
|
+
app.add_middleware(TenantMiddleware)
|
|
89
|
+
app.add_middleware(TenantMiddleware, exclude_paths={"/health", "/custom"})
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, app, exclude_paths: set[str] | None = None) -> None:
|
|
93
|
+
self.app = app
|
|
94
|
+
self.excluded = frozenset(exclude_paths) if exclude_paths is not None else _DEFAULT_EXCLUDE
|
|
95
|
+
|
|
96
|
+
async def __call__(self, scope, receive, send):
|
|
97
|
+
if scope["type"] != "http":
|
|
98
|
+
await self.app(scope, receive, send)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
path = scope.get("path", "")
|
|
102
|
+
if path in self.excluded:
|
|
103
|
+
await self.app(scope, receive, send)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
headers = scope.get("headers", [])
|
|
107
|
+
raw_tid = _get_header(headers, b"x-tenant-id")
|
|
108
|
+
|
|
109
|
+
if not raw_tid or not _validate_uuid(raw_tid):
|
|
110
|
+
body = json.dumps({"error": "X-Tenant-ID header required"}).encode()
|
|
111
|
+
await send({
|
|
112
|
+
"type": "http.response.start",
|
|
113
|
+
"status": 401,
|
|
114
|
+
"headers": [
|
|
115
|
+
(b"content-type", b"application/json"),
|
|
116
|
+
(b"content-length", str(len(body)).encode()),
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
await send({"type": "http.response.body", "body": body})
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
raw_rid = _get_header(headers, b"x-request-id") or str(uuid.uuid4())
|
|
123
|
+
|
|
124
|
+
tid_token = _tenant_id_var.set(raw_tid)
|
|
125
|
+
rid_token = _request_id_var.set(raw_rid)
|
|
126
|
+
|
|
127
|
+
async def send_with_request_id(message):
|
|
128
|
+
if message.get("type") == "http.response.start":
|
|
129
|
+
resp_headers = list(message.get("headers", []))
|
|
130
|
+
resp_headers.append((b"x-request-id", raw_rid.encode()))
|
|
131
|
+
message = {**message, "headers": resp_headers}
|
|
132
|
+
await send(message)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
await self.app(scope, receive, send_with_request_id)
|
|
136
|
+
finally:
|
|
137
|
+
_tenant_id_var.reset(tid_token)
|
|
138
|
+
_request_id_var.reset(rid_token)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def middleware(exclude_paths: set[str] | None = None) -> Callable:
|
|
142
|
+
"""ASGI middleware factory. Prefer TenantMiddleware class with app.add_middleware().
|
|
143
|
+
|
|
144
|
+
This factory form works for raw ASGI wrapping (tests).
|
|
145
|
+
For FastAPI, use: app.add_middleware(TenantMiddleware)
|
|
146
|
+
|
|
147
|
+
Rules: This function returns a factory for ASGI middleware; prefer using TenantMiddleware class directly with app.add_middleware() for FastAPI apps.
|
|
148
|
+
"""
|
|
149
|
+
excluded = exclude_paths
|
|
150
|
+
|
|
151
|
+
def asgi_middleware(app):
|
|
152
|
+
mw = TenantMiddleware(app, exclude_paths=excluded)
|
|
153
|
+
return mw
|
|
154
|
+
|
|
155
|
+
return asgi_middleware
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def set_rls(session) -> None:
|
|
159
|
+
"""Set PostgreSQL RLS tenant context. Must be called inside an active transaction.
|
|
160
|
+
|
|
161
|
+
Uses transaction-local set_config so the setting resets when the transaction ends.
|
|
162
|
+
Requires sqlalchemy (optional dependency).
|
|
163
|
+
|
|
164
|
+
Rules: Must be called inside an active SQLAlchemy transaction; otherwise raises RuntimeError. Sets PostgreSQL RLS context using set_config within the transaction.
|
|
165
|
+
"""
|
|
166
|
+
from sqlalchemy import text
|
|
167
|
+
|
|
168
|
+
if not session.in_transaction():
|
|
169
|
+
raise RuntimeError(
|
|
170
|
+
"set_rls() must be called inside an active transaction. "
|
|
171
|
+
"Use 'async with session.begin():' before calling."
|
|
172
|
+
)
|
|
173
|
+
tid = require()
|
|
174
|
+
await session.execute(
|
|
175
|
+
text("SELECT set_config('app.tenant_id', :tid, true)"),
|
|
176
|
+
{"tid": tid},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def set_rls_conn(conn: "asyncpg.Connection") -> None:
|
|
181
|
+
"""Set PostgreSQL RLS tenant context on an asyncpg connection.
|
|
182
|
+
|
|
183
|
+
Asyncpg counterpart of set_rls(). Reads tenant from ContextVar (require()).
|
|
184
|
+
Must be called inside an active transaction — set_config(..., true) is
|
|
185
|
+
transaction-local; outside a transaction the setting silently leaks across
|
|
186
|
+
pooled checkouts (cross-tenant data leak risk).
|
|
187
|
+
|
|
188
|
+
Rules: Must be called inside an active asyncpg transaction; otherwise raises RuntimeError. Sets PostgreSQL RLS context using set_config within the transaction to prevent cross-tenant data leaks.
|
|
189
|
+
"""
|
|
190
|
+
if not conn.is_in_transaction():
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
"set_rls_conn() must be called inside an active transaction. "
|
|
193
|
+
"Use 'async with conn.transaction():' before calling."
|
|
194
|
+
)
|
|
195
|
+
tid = require()
|
|
196
|
+
await conn.execute("SELECT set_config('app.tenant_id', $1, true)", tid)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@contextmanager
|
|
200
|
+
def bind(tenant_id: str, request_id: str | None = None):
|
|
201
|
+
"""Set tenant context for background tasks, scripts, and tests.
|
|
202
|
+
|
|
203
|
+
Resets on exit, even if an exception is raised.
|
|
204
|
+
|
|
205
|
+
Rules: Sets tenant and request ID in ContextVars for background tasks, scripts, or tests; resets values on exit even if an exception occurs.
|
|
206
|
+
"""
|
|
207
|
+
tid_token = _tenant_id_var.set(tenant_id)
|
|
208
|
+
rid_token = _request_id_var.set(request_id)
|
|
209
|
+
try:
|
|
210
|
+
yield
|
|
211
|
+
finally:
|
|
212
|
+
_tenant_id_var.reset(tid_token)
|
|
213
|
+
_request_id_var.reset(rid_token)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@asynccontextmanager
|
|
217
|
+
async def async_bind(
|
|
218
|
+
tenant_id: str, request_id: str | None = None
|
|
219
|
+
) -> AsyncIterator[None]:
|
|
220
|
+
"""Async counterpart of bind() for use inside `async with` blocks.
|
|
221
|
+
|
|
222
|
+
ContextVar set/reset itself is sync; this is a convenience wrapper so
|
|
223
|
+
job-queue middleware and other async code can write `async with
|
|
224
|
+
tenant.async_bind(tid):` without a `with` inside `async def`.
|
|
225
|
+
|
|
226
|
+
Rules: Async version of bind(); used in async contexts with `async with tenant.async_bind(tid):` to manage tenant context.
|
|
227
|
+
"""
|
|
228
|
+
tid_token = _tenant_id_var.set(tenant_id)
|
|
229
|
+
rid_token = _request_id_var.set(request_id)
|
|
230
|
+
try:
|
|
231
|
+
yield
|
|
232
|
+
finally:
|
|
233
|
+
_tenant_id_var.reset(tid_token)
|
|
234
|
+
_request_id_var.reset(rid_token)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def hashed_for_log(tenant_id: str, key: bytes | None = None) -> str:
|
|
238
|
+
"""HMAC-SHA-256 of tenant_id for log/metric correlation without re-id risk.
|
|
239
|
+
|
|
240
|
+
Plain SHA-256 over a finite tenant population is brute-forceable; HMAC with
|
|
241
|
+
a per-deployment secret is not. Reads LOG_HMAC_KEY from env when key
|
|
242
|
+
not supplied. Returns first 16 hex chars (8 bytes) — enough entropy for
|
|
243
|
+
correlation, short enough for log lines.
|
|
244
|
+
|
|
245
|
+
Key encoding contract: LOG_HMAC_KEY must be a hex string (output of
|
|
246
|
+
`openssl rand -hex 32`). hashed_for_log decodes it to raw bytes before
|
|
247
|
+
HMAC, so the full 256 bits of entropy are used. A bare ASCII passphrase
|
|
248
|
+
will silently work but only at ~5 bits/char effective key strength.
|
|
249
|
+
|
|
250
|
+
Rules: Requires LOG_HMAC_KEY environment variable to be set as a hex-encoded 32-byte key; returns first 16 hex chars of HMAC-SHA-256 for log correlation.
|
|
251
|
+
"""
|
|
252
|
+
if key is None:
|
|
253
|
+
env = os.environ.get("LOG_HMAC_KEY")
|
|
254
|
+
if not env:
|
|
255
|
+
raise RuntimeError(
|
|
256
|
+
"LOG_HMAC_KEY env var required for tenant log hashing. "
|
|
257
|
+
"Set in BaseServiceSettings or pass key= explicitly."
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
key = bytes.fromhex(env)
|
|
261
|
+
except ValueError as exc:
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
"LOG_HMAC_KEY must be a hex string (openssl rand -hex 32). "
|
|
264
|
+
f"Got {len(env)} chars, decode error: {exc}"
|
|
265
|
+
) from exc
|
|
266
|
+
if len(key) < 16:
|
|
267
|
+
raise RuntimeError(
|
|
268
|
+
f"LOG_HMAC_KEY too short ({len(key)} bytes); need >= 16"
|
|
269
|
+
)
|
|
270
|
+
digest = hmac.new(key, tenant_id.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
271
|
+
return digest[:16]
|
omkit/tracing.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""packages/omur-sdk/omkit/tracing.py — OpenTelemetry tracing bootstrap for Omur services.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from omkit.tracing import init_tracing
|
|
5
|
+
init_tracing("spine") # Call once at startup
|
|
6
|
+
|
|
7
|
+
Tracing is OFF by default since the 2026-04 infra consolidation (Alloy and
|
|
8
|
+
Tempo were removed). Set OTEL_EXPORTER_OTLP_ENDPOINT to a reachable OTLP/HTTP
|
|
9
|
+
collector (e.g. ``http://otel-collector:4318``) to re-enable span export.
|
|
10
|
+
|
|
11
|
+
exports: DEFAULT_ENDPOINT | init_tracing(service_name, endpoint) | instrument_fastapi(app)
|
|
12
|
+
rules: The tracing module must maintain backward compatibility with all existing FastAPI instrumentation patterns and cannot introduce breaking changes to the existing service_name and endpoint parameter signatures. The module requires explicit error handling for endpoint connection failures and must not modify global tracing state outside of the init_tracing and instrument_fastapi functions. All tracing operations must be thread-safe and support concurrent FastAPI application instances.
|
|
13
|
+
agent: ollama/qwen3-coder:latest | ollama | 2026-05-01 | codedna-cli | initial CodeDNA annotation pass
|
|
14
|
+
message:
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import structlog
|
|
21
|
+
|
|
22
|
+
log = structlog.get_logger()
|
|
23
|
+
|
|
24
|
+
DEFAULT_ENDPOINT = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def init_tracing(
|
|
28
|
+
service_name: str,
|
|
29
|
+
endpoint: str | None = None,
|
|
30
|
+
) -> "TracerProvider | None":
|
|
31
|
+
"""Initialize OpenTelemetry with OTLP/HTTP export.
|
|
32
|
+
|
|
33
|
+
Returns the TracerProvider, or None if tracing is disabled.
|
|
34
|
+
|
|
35
|
+
Rules: Must ensure OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set when endpoint is None and DEFAULT_ENDPOINT is not provided, otherwise tracing will be silently disabled.
|
|
36
|
+
"""
|
|
37
|
+
if endpoint is None:
|
|
38
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", DEFAULT_ENDPOINT)
|
|
39
|
+
|
|
40
|
+
if not endpoint:
|
|
41
|
+
log.info("tracing.disabled", service=service_name)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from opentelemetry import trace
|
|
46
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
47
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
48
|
+
from opentelemetry.sdk.resources import Resource
|
|
49
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
50
|
+
except ImportError:
|
|
51
|
+
log.info("tracing.not_installed", service=service_name)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
resource = Resource.create({"service.name": service_name})
|
|
55
|
+
provider = TracerProvider(resource=resource)
|
|
56
|
+
exporter = OTLPSpanExporter(endpoint=f"{endpoint}/v1/traces")
|
|
57
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
58
|
+
trace.set_tracer_provider(provider)
|
|
59
|
+
|
|
60
|
+
log.info("tracing.enabled", service=service_name, endpoint=endpoint)
|
|
61
|
+
return provider
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def instrument_fastapi(app) -> None:
|
|
65
|
+
"""Wrap a FastAPI app with OpenTelemetry server-side instrumentation.
|
|
66
|
+
|
|
67
|
+
Idempotent: calling twice on the same app is a no-op. Silently no-ops if
|
|
68
|
+
opentelemetry-instrumentation-fastapi is not installed (in-tree optional).
|
|
69
|
+
|
|
70
|
+
Rules: Function is idempotent but requires opentelemetry-instrumentation-fastapi package to be installed, otherwise it will silently no-op without raising an error.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
74
|
+
except ImportError:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if getattr(app, "_omur_otel_instrumented", False):
|
|
78
|
+
return
|
|
79
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
80
|
+
app._omur_otel_instrumented = True
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""packages/omur-sdk/omkit/transport/__init__.py — re-exports cross-cutting wire / observability primitives.
|
|
2
|
+
|
|
3
|
+
This is an additive grouping for discoverability. Existing imports from the
|
|
4
|
+
flat modules (``omkit.http``, ``omkit.tracing``, etc.) continue to work
|
|
5
|
+
unchanged; new code is encouraged to import from this facade.
|
|
6
|
+
|
|
7
|
+
exports: none
|
|
8
|
+
rules: The transport module must maintain backward compatibility for all existing API endpoints and response formats, as breaking changes will affect downstream services that depend on stable interfaces. All network communication must go through a centralized connection pooling mechanism to ensure resource efficiency and proper handling of concurrent requests. The module cannot introduce any synchronous blocking operations that would impact the overall performance of applications using the SDK.
|
|
9
|
+
agent: ollama/qwen3-coder:latest | ollama | 2026-05-01 | codedna-cli | initial CodeDNA annotation pass
|
|
10
|
+
message:
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from omkit.health import mount_health_endpoints
|
|
14
|
+
from omkit.http import build_tenant_client
|
|
15
|
+
from omkit.logging import configure_logging
|
|
16
|
+
from omkit.metrics import mount_metrics
|
|
17
|
+
from omkit.resilience import CircuitBreaker, resilient
|
|
18
|
+
from omkit.tracing import init_tracing, instrument_fastapi
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"build_tenant_client",
|
|
22
|
+
"init_tracing",
|
|
23
|
+
"instrument_fastapi",
|
|
24
|
+
"mount_metrics",
|
|
25
|
+
"mount_health_endpoints",
|
|
26
|
+
"configure_logging",
|
|
27
|
+
"CircuitBreaker",
|
|
28
|
+
"resilient",
|
|
29
|
+
]
|
omkit/valkey.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""packages/omur-sdk/omkit/valkey.py — Valkey client factory.
|
|
2
|
+
|
|
3
|
+
Single source of truth for `redis.asyncio.Redis` construction across the SDK.
|
|
4
|
+
Replaces the URL-construction duplication in `eventbus.new_bus()` and other
|
|
5
|
+
call sites. Reads BaseServiceSettings.valkey_url so password handling stays
|
|
6
|
+
consistent.
|
|
7
|
+
|
|
8
|
+
Note: streaq does not use this factory — streaq depends on `coredis`, a
|
|
9
|
+
different async Redis client. Services that use streaq construct the Worker
|
|
10
|
+
directly from `settings.valkey_url`.
|
|
11
|
+
|
|
12
|
+
exports: new_client(settings)
|
|
13
|
+
rules: The module must maintain backward compatibility with existing Redis connection patterns while ensuring all async operations are properly awaited. The client initialization must respect the settings structure defined in the SDK's configuration schema. All Redis operations must be wrapped with appropriate timeout and retry logic to prevent service disruptions.
|
|
14
|
+
agent: ollama/qwen3-coder:latest | ollama | 2026-05-01 | codedna-cli | initial CodeDNA annotation pass
|
|
15
|
+
message:
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import redis.asyncio as aioredis
|
|
24
|
+
|
|
25
|
+
from omkit.config import BaseServiceSettings
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def new_client(
|
|
29
|
+
settings: "BaseServiceSettings", **kwargs: Any
|
|
30
|
+
) -> "aioredis.Redis":
|
|
31
|
+
"""Build a redis.asyncio.Redis client from BaseServiceSettings.
|
|
32
|
+
|
|
33
|
+
Reuses settings.valkey_url to keep password handling and host/port logic
|
|
34
|
+
in one place. Empty password falls back to no-auth URL — fail-fast on
|
|
35
|
+
empty password is enforced at compose-startup via the
|
|
36
|
+
`${VALKEY_PASSWORD:?VALKEY_PASSWORD required}` interpolation, not here.
|
|
37
|
+
|
|
38
|
+
Extra kwargs pass through to redis.asyncio.from_url (decode_responses,
|
|
39
|
+
socket_timeout, etc.).
|
|
40
|
+
|
|
41
|
+
Rules: The function relies on settings.valkey_url being properly configured with a valid Redis connection string. It assumes that password handling and host/port logic are correctly implemented in the BaseServiceSettings, and that the VALKEY_PASSWORD environment variable is enforced at startup to prevent empty passwords.
|
|
42
|
+
"""
|
|
43
|
+
import redis.asyncio as aioredis
|
|
44
|
+
|
|
45
|
+
return aioredis.from_url(settings.valkey_url, **kwargs)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omkit
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Multi-tenant SaaS scaffolding for Python services.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: pydantic>=2.13
|
|
7
|
+
Requires-Dist: pydantic-settings>=2.13
|
|
8
|
+
Requires-Dist: asyncpg>=0.31
|
|
9
|
+
Requires-Dist: redis>=7.4
|
|
10
|
+
Requires-Dist: structlog>=25.5
|
|
11
|
+
Requires-Dist: cryptography>=47.0
|
|
12
|
+
Requires-Dist: tenacity>=9.1
|
|
13
|
+
Requires-Dist: prometheus_client>=0.25
|
|
14
|
+
Requires-Dist: httpx>=0.28.1
|
|
15
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.49
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: fastapi>=0.136.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=1.3; extra == "dev"
|
|
20
|
+
Requires-Dist: redis>=7.4; extra == "dev"
|
|
21
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
22
|
+
Provides-Extra: tracing
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.41.0; extra == "tracing"
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.41.0; extra == "tracing"
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.41.0; extra == "tracing"
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.62b0; extra == "tracing"
|
|
27
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.62b0; extra == "tracing"
|
|
28
|
+
Provides-Extra: metrics
|
|
29
|
+
Requires-Dist: prometheus-fastapi-instrumentator>=7.1; extra == "metrics"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
omkit/__init__.py,sha256=9wLFyshKmCV8EM4wtNs_Hp_yUlTec4Yq2BxrwnhUz10,518
|
|
2
|
+
omkit/cleanup.py,sha256=7Pss_w6dZx9VRGrHPfLD8fILdVsOYzUJrs953rih8NE,2415
|
|
3
|
+
omkit/config.py,sha256=TD554an8GC7WdKkuhDFbnErDvLZkEWjFXEFIq9ReCfo,1846
|
|
4
|
+
omkit/cost.py,sha256=b4EoJekhj-UdqJDM3ETyuFT7Ea9ccko7osr-nX1-3F8,2752
|
|
5
|
+
omkit/dbpool.py,sha256=vI3HNbolu1OmWnJbEz1eD_X2QZNFrZohHzsPVyHXVEo,6182
|
|
6
|
+
omkit/encryption.py,sha256=kM_iOXk80zJn4NhIYFFmonho-uTdcXG14IaYeI6umms,2546
|
|
7
|
+
omkit/eventbus.py,sha256=-A6ALK3dNk8xjkuNOOkL_i8SnN2WREhb3bzjwh7ZuNI,16296
|
|
8
|
+
omkit/events.py,sha256=Qg9SEnsLX-KmvpRzOBDqEcJlBbx5l7c0X4aJV4PZvuo,1117
|
|
9
|
+
omkit/health.py,sha256=PP7fa2JkWtKf6lLZsa6GjxndSL0UD7iVuA5wGhX5RyU,3236
|
|
10
|
+
omkit/http.py,sha256=mEpxp2LZ25ZBEu0lVbsq_lqGIfIW8b3npQ7mKKzdYGM,3423
|
|
11
|
+
omkit/logging.py,sha256=LtH8uCix9U1Y-osvY2YIgz9aakYgtZ-eRjSUPydwXpY,3290
|
|
12
|
+
omkit/metrics.py,sha256=LftIRzSRDFDj1e1QGv5a8CEUkWSuI-vpYZAUqicMJss,1432
|
|
13
|
+
omkit/model_lifecycle.py,sha256=VB20gfP4GeVDr0d77sKkDHkmU_jk7JKglTrHWaIiaMs,6163
|
|
14
|
+
omkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
omkit/quota.py,sha256=R2rxRmn7yBiU-5DmUVvGL-j0rpNZCPvS22Nhx1tIZmA,6682
|
|
16
|
+
omkit/resilience.py,sha256=P7IdHjRop80wIuWIHPKxO4629OgHsVGr1TmzRmQCxzo,5238
|
|
17
|
+
omkit/sanitize.py,sha256=hE4RrRcRnqT4Wk5F1fTXUtbxpTo5Dh4UXdkYybRn81M,4646
|
|
18
|
+
omkit/sessions.py,sha256=vv2lF6e8-reGNQr_Nba3C4Jg7veCixi1sPwi4XYArac,15489
|
|
19
|
+
omkit/settings.py,sha256=wEBemRcALeaA2CAXikcQ1ktfLARgzXFzF4RK3HjC3Uk,14792
|
|
20
|
+
omkit/sync_notifier.py,sha256=Zr-mVzJDClC82aQO80944b8oFrWJhY2RnkVEcuF0sX4,5228
|
|
21
|
+
omkit/tenant.py,sha256=jpV-tXsmudFScQGmN3jj-VDWpiRbT1QOQHbzikH9C3o,10305
|
|
22
|
+
omkit/tracing.py,sha256=J2atOwD13wdnMpx68AUCEDo81AP0MQBK-JjkOB4fo5w,3415
|
|
23
|
+
omkit/valkey.py,sha256=KbRWbNEcjlYEhApfIz6LKjiodSxkql2Z7sTmDB1gL-Y,2183
|
|
24
|
+
omkit/data/__init__.py,sha256=jAX729bco_Jv4m-l1mhnUvOGuUSbuOjrCeFGObYin50,1106
|
|
25
|
+
omkit/internal/__init__.py,sha256=JCAYaVasGvY1GJ5SCXHV7Q1uQSEy1BYJICelXSPjIqM,258
|
|
26
|
+
omkit/internal/crypto.py,sha256=ddPwA4JQgy8t116OxCXUFplsrorrADW3wanHMXdlBxg,904
|
|
27
|
+
omkit/jobqueue/__init__.py,sha256=XHYU229TG73AVM8g1Dhn636MArvzPrdMp2PGTKTzGPQ,1174
|
|
28
|
+
omkit/jobqueue/envelope.py,sha256=-8FVv2ZhaC9KtVzYkU8qOaRNiECx0h1KrjPnDi5M3s4,4608
|
|
29
|
+
omkit/jobqueue/streaq.py,sha256=g_t8DK-1K1-SYuiTHHUjVktiaqAi9HCFLzfj8TLMTO0,11104
|
|
30
|
+
omkit/platform/__init__.py,sha256=BN6cqsUyGWMgVRb_hQKiSDVyo51-BSOQ708cv4N5EBI,490
|
|
31
|
+
omkit/providers/__init__.py,sha256=1JMezeFpDRzIF15f1LNBe-lIIWUf_Bfaqtjx2rSc-OM,419
|
|
32
|
+
omkit/providers/base.py,sha256=YG01V18FYHQHxUNV9-DGTdtjeu-BuqjT0py5lhelATE,2787
|
|
33
|
+
omkit/providers/registry.py,sha256=peM134Z9Zib-9krjyImlqLZGeBL8uzw0zFCd-cIQib4,11257
|
|
34
|
+
omkit/security/__init__.py,sha256=pmE98gw2DO87fMMKcZjcOkg2_L8wV919Ig5afI_sKkA,1231
|
|
35
|
+
omkit/security/events.py,sha256=ur0oRFuoaj-E5KhrxPoZd0mphkGNHsGWH9A4UGf-V5s,2687
|
|
36
|
+
omkit/transport/__init__.py,sha256=YF5U0wCaJzEn2aMBhgMkky6AyT4JUyCycoRqNbMjD4U,1451
|
|
37
|
+
omkit-0.0.2.dist-info/METADATA,sha256=vz3GMY3pUiFjUzkTaJ8ZjVqdNQt95toW7Y9XyrnJUVs,1195
|
|
38
|
+
omkit-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
39
|
+
omkit-0.0.2.dist-info/top_level.txt,sha256=sCEPnxCXPMDdG0h3BXj0COELTJHdOn5q_LycbET5zrg,6
|
|
40
|
+
omkit-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
omkit
|