nlbone 0.1.34__py3-none-any.whl → 0.1.35__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.
nlbone/container.py CHANGED
@@ -1,13 +1,31 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Mapping, Optional
3
+
4
+ from dependency_injector import containers, providers
5
+
1
6
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
2
7
  from nlbone.adapters.http_clients.uploadchi_async import UploadchiAsyncClient
3
- from nlbone.config.settings import Settings
4
8
  from nlbone.adapters.auth.keycloak import KeycloakAuthService
5
9
  from nlbone.core.ports.files import FileServicePort, AsyncFileServicePort
6
10
 
7
11
 
8
- class Container:
9
- def __init__(self, settings: Settings | None = None):
10
- self.settings = settings or Settings()
11
- self.auth: KeycloakAuthService = KeycloakAuthService(self.settings)
12
- self.file_service: FileServicePort = UploadchiClient()
13
- self.afiles_service: AsyncFileServicePort = UploadchiAsyncClient()
12
+ class Container(containers.DeclarativeContainer):
13
+ config = providers.Configuration(strict=False)
14
+
15
+ auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService, settings=config)
16
+ file_service: providers.Singleton[FileServicePort] = providers.Singleton(UploadchiClient)
17
+ afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(UploadchiAsyncClient)
18
+
19
+
20
+ def create_container(settings: Optional[Any] = None) -> Container:
21
+ c = Container()
22
+ if settings is not None:
23
+ if hasattr(settings, "model_dump"):
24
+ c.config.from_dict(settings.model_dump()) # Pydantic v2
25
+ elif hasattr(settings, "dict"):
26
+ c.config.from_dict(settings.dict()) # Pydantic v1
27
+ elif isinstance(settings, Mapping):
28
+ c.config.from_dict(dict(settings))
29
+ else:
30
+ c.config.override(settings)
31
+ return c
File without changes
@@ -0,0 +1,30 @@
1
+ import time
2
+ from typing import Callable
3
+ from fastapi import Request
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+
6
+ from nlbone.config.logging import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+ class AccessLogMiddleware(BaseHTTPMiddleware):
11
+ async def dispatch(self, request: Request, call_next: Callable):
12
+ start = time.perf_counter()
13
+ status_code = None
14
+ try:
15
+ response = await call_next(request)
16
+ status_code = getattr(response, "status_code", None)
17
+ return response
18
+ except Exception:
19
+ status_code = 500
20
+ raise
21
+ finally:
22
+ dur_ms = int((time.perf_counter() - start) * 1000)
23
+ logger.info({
24
+ "event": "access",
25
+ "method": request.method,
26
+ "path": request.url.path,
27
+ "status": status_code,
28
+ "duration_ms": dur_ms,
29
+ "query": request.url.query,
30
+ })
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from uuid import uuid4
4
+
5
+ from starlette.datastructures import Headers
6
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
7
+ from starlette.requests import Request
8
+
9
+ from nlbone import config
10
+ from nlbone.utils.context import request_ctx, request_id_ctx, bind_context, reset_context
11
+
12
+
13
+ def mock_request(user=None, token: Optional[str] = None):
14
+ request = Request({
15
+ "type": "http",
16
+ "headers": Headers({"User-Agent": "Testing-Agent"}).raw,
17
+ "client": {"host": "192.168.1.1", "port": 80},
18
+ "method": "GET",
19
+ "path": "/__test__",
20
+ })
21
+ request.state.user = user
22
+ request.state.token = token
23
+ return request
24
+
25
+
26
+ def current_request() -> Optional[Request]:
27
+ req = request_ctx.get()
28
+ if config.ENV == 'test' and req is None:
29
+ return mock_request()
30
+ return req
31
+
32
+
33
+ def current_request_id() -> Optional[str]:
34
+ return request_id_ctx.get()
35
+
36
+
37
+ class AddRequestContextMiddleware(BaseHTTPMiddleware):
38
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
39
+ incoming_req_id = request.headers.get("X-Request-ID")
40
+ req_id = incoming_req_id or str(uuid4())
41
+
42
+ user_id = getattr(getattr(request, "state", None), "user_id", None) or request.headers.get("X-User-Id")
43
+ ip = request.client.host if request.client else None
44
+ ua = request.headers.get("user-agent")
45
+
46
+ tokens = bind_context(request=request, request_id=req_id, user_id=user_id, ip=ip, user_agent=ua)
47
+ try:
48
+ response = await call_next(request)
49
+ response.headers.setdefault("X-Request-ID", req_id)
50
+ return response
51
+ finally:
52
+ reset_context(tokens)
@@ -0,0 +1,56 @@
1
+ from typing import Optional, Callable, Union
2
+ from fastapi import Request
3
+ from starlette.middleware.base import BaseHTTPMiddleware
4
+
5
+ try:
6
+ from dependency_injector import providers
7
+ ProviderType = providers.Provider # type: ignore
8
+ except Exception:
9
+ ProviderType = object
10
+
11
+ from nlbone.core.ports.auth import AuthService
12
+
13
+
14
+ def _to_factory(auth: Union[AuthService, Callable[[], AuthService], ProviderType]):
15
+ try:
16
+ from dependency_injector import providers as _p # type: ignore
17
+ if isinstance(auth, _p.Provider):
18
+ return auth
19
+ except Exception:
20
+ pass
21
+ if callable(auth) and not hasattr(auth, "verify_token"):
22
+ return auth
23
+ return lambda: auth
24
+
25
+
26
+ class AuthenticationMiddleware(BaseHTTPMiddleware):
27
+ def __init__(self, app, auth: Union[AuthService, Callable[[], AuthService], ProviderType]):
28
+ super().__init__(app)
29
+ self._get_auth = _to_factory(auth)
30
+
31
+ async def dispatch(self, request: Request, call_next):
32
+ request.state.client_id = request.headers.get("X-Client-Id")
33
+ request.state.user_id = None
34
+ request.state.token = None
35
+
36
+ token: Optional[str] = None
37
+ authz = request.headers.get("Authorization")
38
+ if authz:
39
+ try:
40
+ scheme, token = authz.split(" ", 1)
41
+ if scheme.lower() != "bearer":
42
+ token = None
43
+ except ValueError:
44
+ token = None
45
+
46
+ if token:
47
+ request.state.token = token
48
+ try:
49
+ service: AuthService = self._get_auth()
50
+ data = service.verify_token(token)
51
+ if data:
52
+ request.state.user_id = data.get("user_id")
53
+ except Exception:
54
+ pass
55
+
56
+ return await call_next(request)
@@ -0,0 +1,42 @@
1
+ from contextvars import ContextVar
2
+ from typing import Any, Optional
3
+
4
+ request_ctx: ContextVar[Any | None] = ContextVar("request", default=None)
5
+ request_id_ctx: ContextVar[str | None] = ContextVar("request_id", default=None)
6
+ user_id_ctx: ContextVar[str | None] = ContextVar("user_id", default=None)
7
+ ip_ctx: ContextVar[str | None] = ContextVar("ip", default=None)
8
+ user_agent_ctx: ContextVar[str | None] = ContextVar("user_agent", default=None)
9
+
10
+ def bind_context(*, request: Any | None = None, request_id: str | None = None,
11
+ user_id: str | None = None, ip: str | None = None,
12
+ user_agent: str | None = None) -> dict[ContextVar, Any]:
13
+ tokens: dict[ContextVar, Any] = {}
14
+ if request is not None:
15
+ tokens[request_ctx] = request_ctx.set(request)
16
+ if request_id is not None:
17
+ tokens[request_id_ctx] = request_id_ctx.set(request_id)
18
+ if user_id is not None:
19
+ tokens[user_id_ctx] = user_id_ctx.set(user_id)
20
+ if ip is not None:
21
+ tokens[ip_ctx] = ip_ctx.set(ip)
22
+ if user_agent is not None:
23
+ tokens[user_agent_ctx] = user_agent_ctx.set(user_agent)
24
+ return tokens
25
+
26
+ def reset_context(tokens: dict[ContextVar, Any]):
27
+ for var, token in tokens.items():
28
+ var.reset(token)
29
+
30
+ def current_request():
31
+ return request_ctx.get()
32
+
33
+ def current_request_id() -> Optional[str]:
34
+ return request_id_ctx.get()
35
+
36
+ def current_context_dict() -> dict[str, Any]:
37
+ return {
38
+ "request_id": request_id_ctx.get(),
39
+ "user_id": user_id_ctx.get(),
40
+ "ip": ip_ctx.get(),
41
+ "user_agent": user_agent_ctx.get(),
42
+ }
nlbone/utils/time.py CHANGED
@@ -0,0 +1,5 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def now() -> datetime:
5
+ return datetime.now(timezone.utc)
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.1.34
3
+ Version: 0.1.35
4
4
  Summary: Backbone package for interfaces and infrastructure in Python projects
5
5
  Author-email: Amir Hosein Kahkbazzadeh <a.khakbazzadeh@gmail.com>
6
6
  License: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: anyio>=4.0
10
+ Requires-Dist: dependency-injector>=4.48.1
10
11
  Requires-Dist: fastapi>=0.116
11
12
  Requires-Dist: httpx>=0.27
12
13
  Requires-Dist: psycopg>=3.2.9
@@ -1,5 +1,5 @@
1
1
  nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nlbone/container.py,sha256=JLEiJSh7vZAl7kBY6bwqtL3IzHFOxhrfreaZDU6K9n0,663
2
+ nlbone/container.py,sha256=KO8y8hfEt0graWhUi-FU_rG-WPckl-uF7H9JGcwEu38,1321
3
3
  nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  nlbone/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  nlbone/adapters/auth/__init__.py,sha256=Eh9kWjY1I8vi17gK0oOzBLJwJX_GFuUcJIN7cLU6lJg,41
@@ -48,6 +48,10 @@ nlbone/interfaces/api/routers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
48
48
  nlbone/interfaces/api/schemas.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  nlbone/interfaces/api/dependencies/__init__.py,sha256=XrTGkHS8xqryfyr8XSm_s9sIzp4E0b44XQ40sVWcKMY,46
50
50
  nlbone/interfaces/api/dependencies/db.py,sha256=IqDVq1lcCCxd22FBUg523lVANM_j71BYAQtsbrHc4M8,465
51
+ nlbone/interfaces/api/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ nlbone/interfaces/api/middleware/access_log.py,sha256=dEjk_m4fyQ72S2xLzDydDoaw2F9Tvmfl_acat1YThE0,972
53
+ nlbone/interfaces/api/middleware/add_request_context.py,sha256=i8EV4BvZyrBcNfU-uTkybr7J7ypYvJq8mXSntM6oel4,1816
54
+ nlbone/interfaces/api/middleware/authentication.py,sha256=-jfV-Ry04qsFd1cJh7KGoGpfnUKIogPF2fK42ZTFgBk,1806
51
55
  nlbone/interfaces/api/pagination/__init__.py,sha256=fdTqy5efdYIcUWbK1mVPQMieGd3HV1lZ8vmIhhsuUKs,58
52
56
  nlbone/interfaces/api/pagination/offset_base.py,sha256=W0SJ0rHLMIqoyiPEAjnoKqY57LBXEyT5J-_5bS8r-NY,4535
53
57
  nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -56,8 +60,9 @@ nlbone/interfaces/cli/main.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
56
60
  nlbone/interfaces/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
61
  nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
62
  nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- nlbone/utils/time.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
- nlbone-0.1.34.dist-info/METADATA,sha256=5mgHF3w38N6ELApJK62fA5G_z_0eM_QSOwWpWkr-_vQ,2256
61
- nlbone-0.1.34.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
- nlbone-0.1.34.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- nlbone-0.1.34.dist-info/RECORD,,
63
+ nlbone/utils/context.py,sha256=AUiN1jM0ebNMopZQoJSqWTfUHuVrp-HV8x6g7QsbEJ8,1601
64
+ nlbone/utils/time.py,sha256=dC0ucyAmHdNf3wpA_JPinl2VJRubWqx2vcRpJsT3-0k,102
65
+ nlbone-0.1.35.dist-info/METADATA,sha256=7sgZBqyIdOjUc4Gq8LSP_-aZEwIPY9x0BlQinoZAuCo,2299
66
+ nlbone-0.1.35.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
67
+ nlbone-0.1.35.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
+ nlbone-0.1.35.dist-info/RECORD,,