nlbone 0.3.3__py3-none-any.whl → 0.4.1__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/adapters/auth/__init__.py +1 -1
- nlbone/adapters/auth/keycloak.py +3 -2
- nlbone/adapters/db/__init__.py +2 -2
- nlbone/adapters/db/sqlalchemy/__init__.py +4 -1
- nlbone/adapters/db/sqlalchemy/base.py +0 -2
- nlbone/adapters/db/sqlalchemy/engine.py +15 -3
- nlbone/adapters/db/sqlalchemy/query_builder.py +27 -11
- nlbone/adapters/db/sqlalchemy/repository.py +54 -0
- nlbone/adapters/db/sqlalchemy/schema.py +5 -5
- nlbone/adapters/db/sqlalchemy/uow.py +71 -0
- nlbone/adapters/http_clients/uploadchi.py +27 -11
- nlbone/adapters/http_clients/uploadchi_async.py +29 -7
- nlbone/adapters/messaging/__init__.py +1 -0
- nlbone/adapters/messaging/event_bus.py +23 -0
- nlbone/config/logging.py +3 -8
- nlbone/config/settings.py +18 -22
- nlbone/container.py +18 -2
- nlbone/core/application/events.py +20 -0
- nlbone/core/application/use_case.py +12 -0
- nlbone/core/domain/base.py +51 -0
- nlbone/core/ports/__init__.py +5 -1
- nlbone/core/ports/auth.py +1 -0
- nlbone/core/ports/event_bus.py +10 -0
- nlbone/core/ports/files.py +26 -5
- nlbone/core/ports/repo.py +19 -0
- nlbone/core/ports/uow.py +19 -0
- nlbone/interfaces/api/dependencies/__init__.py +11 -2
- nlbone/interfaces/api/dependencies/async_auth.py +61 -0
- nlbone/interfaces/api/dependencies/auth.py +5 -3
- nlbone/interfaces/api/dependencies/db.py +4 -2
- nlbone/interfaces/api/dependencies/uow.py +32 -0
- nlbone/interfaces/api/exception_handlers.py +17 -15
- nlbone/interfaces/api/exceptions.py +1 -2
- nlbone/interfaces/api/middleware/__init__.py +2 -2
- nlbone/interfaces/api/middleware/access_log.py +12 -8
- nlbone/interfaces/api/middleware/add_request_context.py +55 -52
- nlbone/interfaces/api/middleware/authentication.py +4 -1
- nlbone/interfaces/api/pagination/__init__.py +4 -5
- nlbone/interfaces/api/pagination/offset_base.py +0 -2
- nlbone/interfaces/cli/init_db.py +3 -0
- nlbone/utils/context.py +14 -4
- nlbone/utils/time.py +1 -1
- {nlbone-0.3.3.dist-info → nlbone-0.4.1.dist-info}/METADATA +1 -9
- nlbone-0.4.1.dist-info/RECORD +72 -0
- nlbone/core/application/use_cases/__init__.py +0 -0
- nlbone/core/application/use_cases/register_user.py +0 -0
- nlbone-0.3.3.dist-info/RECORD +0 -64
- {nlbone-0.3.3.dist-info → nlbone-0.4.1.dist-info}/WHEEL +0 -0
- {nlbone-0.3.3.dist-info → nlbone-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Generic, List, TypeVar
|
|
6
|
+
|
|
7
|
+
TId = TypeVar("TId")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DomainError(Exception):
|
|
11
|
+
"""Base domain exception."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class DomainEvent:
|
|
16
|
+
"""Immutable domain event."""
|
|
17
|
+
|
|
18
|
+
occurred_at: datetime = datetime.now(timezone.utc)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self):
|
|
22
|
+
return self.__class__.__name__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ValueObject:
|
|
26
|
+
"""Base for value objects (immutable in practice)."""
|
|
27
|
+
|
|
28
|
+
def __eq__(self, other: Any) -> bool:
|
|
29
|
+
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
|
|
30
|
+
|
|
31
|
+
def __hash__(self) -> int: # allow in sets/dicts
|
|
32
|
+
return hash(tuple(sorted(self.__dict__.items())))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Entity(Generic[TId]):
|
|
36
|
+
id: TId
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AggregateRoot(Entity[TId]):
|
|
40
|
+
"""Aggregate root with domain event collection."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
43
|
+
self._domain_events: List[DomainEvent] = []
|
|
44
|
+
|
|
45
|
+
def _raise(self, event: DomainEvent) -> None:
|
|
46
|
+
self._domain_events.append(event)
|
|
47
|
+
|
|
48
|
+
def pull_events(self) -> List[DomainEvent]:
|
|
49
|
+
events = list(self._domain_events)
|
|
50
|
+
self._domain_events.clear()
|
|
51
|
+
return events
|
nlbone/core/ports/__init__.py
CHANGED
nlbone/core/ports/auth.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Iterable, Protocol
|
|
4
|
+
|
|
5
|
+
from nlbone.core.domain.base import DomainEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventBusPort(Protocol):
|
|
9
|
+
def publish(self, events: Iterable[DomainEvent]) -> None: ...
|
|
10
|
+
def subscribe(self, event_name: str, handler: Callable[[DomainEvent], None]) -> None: ...
|
nlbone/core/ports/files.py
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator, Protocol, runtime_checkable
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
@runtime_checkable
|
|
5
7
|
class FileServicePort(Protocol):
|
|
6
|
-
def upload_file(
|
|
8
|
+
def upload_file(
|
|
9
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
10
|
+
) -> dict: ...
|
|
7
11
|
def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None: ...
|
|
8
|
-
def list_files(
|
|
12
|
+
def list_files(
|
|
13
|
+
self,
|
|
14
|
+
limit: int = 10,
|
|
15
|
+
offset: int = 0,
|
|
16
|
+
filters: dict[str, Any] | None = None,
|
|
17
|
+
sort: list[tuple[str, str]] | None = None,
|
|
18
|
+
token: str | None = None,
|
|
19
|
+
) -> dict: ...
|
|
9
20
|
def get_file(self, file_id: int, token: str | None = None) -> dict: ...
|
|
10
21
|
def download_file(self, file_id: int, token: str | None = None) -> tuple[bytes, str, str]: ...
|
|
11
22
|
def delete_file(self, file_id: int, token: str | None = None) -> None: ...
|
|
12
23
|
|
|
24
|
+
|
|
13
25
|
@runtime_checkable
|
|
14
26
|
class AsyncFileServicePort(Protocol):
|
|
15
|
-
async def upload_file(
|
|
27
|
+
async def upload_file(
|
|
28
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
29
|
+
) -> dict: ...
|
|
16
30
|
async def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None: ...
|
|
17
|
-
async def list_files(
|
|
31
|
+
async def list_files(
|
|
32
|
+
self,
|
|
33
|
+
limit: int = 10,
|
|
34
|
+
offset: int = 0,
|
|
35
|
+
filters: dict[str, Any] | None = None,
|
|
36
|
+
sort: list[tuple[str, str]] | None = None,
|
|
37
|
+
token: str | None = None,
|
|
38
|
+
) -> dict: ...
|
|
18
39
|
async def get_file(self, file_id: int, token: str | None = None) -> dict: ...
|
|
19
40
|
async def download_file(self, file_id: int, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]: ...
|
|
20
41
|
async def delete_file(self, file_id: int, token: str | None = None) -> None: ...
|
nlbone/core/ports/repo.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, List, Optional, Protocol, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Repository(Protocol[T]): # ← نه Protocol, Generic[T]
|
|
9
|
+
def get(self, id) -> Optional[T]: ...
|
|
10
|
+
def add(self, obj: T) -> None: ...
|
|
11
|
+
def remove(self, obj: T) -> None: ...
|
|
12
|
+
def list(self, *, limit: int | None = None, offset: int = 0) -> Iterable[T]: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncRepository(Protocol[T]):
|
|
16
|
+
async def get(self, id) -> Optional[T]: ...
|
|
17
|
+
def add(self, obj: T) -> None: ...
|
|
18
|
+
async def remove(self, obj: T) -> None: ...
|
|
19
|
+
async def list(self, *, limit: int | None = None, offset: int = 0) -> List[T]: ...
|
nlbone/core/ports/uow.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class UnitOfWork(Protocol):
|
|
8
|
+
def __enter__(self) -> "UnitOfWork": ...
|
|
9
|
+
def __exit__(self, exc_type, exc, tb) -> None: ...
|
|
10
|
+
def commit(self) -> None: ...
|
|
11
|
+
def rollback(self) -> None: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class AsyncUnitOfWork(Protocol):
|
|
16
|
+
async def __aenter__(self) -> "AsyncUnitOfWork": ...
|
|
17
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: ...
|
|
18
|
+
async def commit(self) -> None: ...
|
|
19
|
+
async def rollback(self) -> None: ...
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .auth import
|
|
1
|
+
from .async_auth import client_has_access, current_request, current_user_id, has_access, user_authenticated
|
|
2
|
+
from .auth import ( # noqa: F811
|
|
3
|
+
client_has_access,
|
|
4
|
+
current_client_id,
|
|
5
|
+
current_request,
|
|
6
|
+
current_user_id,
|
|
7
|
+
has_access,
|
|
8
|
+
user_authenticated,
|
|
9
|
+
)
|
|
10
|
+
from .db import get_async_session, get_session
|
|
11
|
+
from .uow import get_async_uow, get_uow
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from nlbone.adapters.auth import KeycloakAuthService
|
|
4
|
+
from nlbone.interfaces.api.exceptions import ForbiddenException, UnauthorizedException
|
|
5
|
+
from nlbone.utils.context import current_request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def current_user_id() -> int:
|
|
9
|
+
user_id = current_request().state.user_id
|
|
10
|
+
if user_id is not None:
|
|
11
|
+
return int(user_id)
|
|
12
|
+
raise UnauthorizedException()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def current_client_id() -> str:
|
|
16
|
+
request = current_request()
|
|
17
|
+
if client_id := KeycloakAuthService().get_client_id(request.state.token):
|
|
18
|
+
return str(client_id)
|
|
19
|
+
raise UnauthorizedException()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def client_has_access(*, permissions=None):
|
|
23
|
+
def decorator(func):
|
|
24
|
+
@functools.wraps(func)
|
|
25
|
+
async def wrapper(*args, **kwargs):
|
|
26
|
+
request = current_request()
|
|
27
|
+
if not KeycloakAuthService().client_has_access(request.state.token, permissions=permissions):
|
|
28
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
29
|
+
|
|
30
|
+
return await func(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
return wrapper
|
|
33
|
+
|
|
34
|
+
return decorator
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def user_authenticated(func):
|
|
38
|
+
@functools.wraps(func)
|
|
39
|
+
async def wrapper(*args, **kwargs):
|
|
40
|
+
if not await current_user_id():
|
|
41
|
+
raise UnauthorizedException()
|
|
42
|
+
return await func(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def has_access(*, permissions=None):
|
|
48
|
+
def decorator(func):
|
|
49
|
+
@functools.wraps(func)
|
|
50
|
+
async def wrapper(*args, **kwargs):
|
|
51
|
+
request = current_request()
|
|
52
|
+
if not await current_user_id():
|
|
53
|
+
raise UnauthorizedException()
|
|
54
|
+
if not KeycloakAuthService().has_access(request.state.token, permissions=permissions):
|
|
55
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
56
|
+
|
|
57
|
+
return await func(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
return wrapper
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
@@ -30,15 +30,16 @@ def client_has_access(*, permissions=None):
|
|
|
30
30
|
return func(*args, **kwargs)
|
|
31
31
|
|
|
32
32
|
return wrapper
|
|
33
|
+
|
|
33
34
|
return decorator
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def user_authenticated(func):
|
|
37
38
|
@functools.wraps(func)
|
|
38
|
-
|
|
39
|
+
def wrapper(*args, **kwargs):
|
|
39
40
|
if not current_user_id():
|
|
40
41
|
raise UnauthorizedException()
|
|
41
|
-
return
|
|
42
|
+
return func(*args, **kwargs)
|
|
42
43
|
|
|
43
44
|
return wrapper
|
|
44
45
|
|
|
@@ -56,4 +57,5 @@ def has_access(*, permissions=None):
|
|
|
56
57
|
return func(*args, **kwargs)
|
|
57
58
|
|
|
58
59
|
return wrapper
|
|
59
|
-
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import AsyncGenerator, Generator
|
|
3
4
|
|
|
4
|
-
from sqlalchemy.orm import Session
|
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
6
7
|
|
|
7
8
|
from nlbone.adapters.db.sqlalchemy.engine import async_session, sync_session
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
|
|
11
12
|
async with async_session() as s:
|
|
12
13
|
yield s
|
|
13
14
|
|
|
15
|
+
|
|
14
16
|
def get_session() -> Generator[Session, None, None]:
|
|
15
17
|
with sync_session() as s:
|
|
16
|
-
yield s
|
|
18
|
+
yield s
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from typing import AsyncIterator
|
|
5
|
+
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
|
|
8
|
+
from nlbone.adapters.db.sqlalchemy import AsyncSqlAlchemyUnitOfWork
|
|
9
|
+
from nlbone.core.ports.uow import AsyncUnitOfWork, UnitOfWork
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_uow(request: Request) -> Iterator[UnitOfWork]:
|
|
13
|
+
"""
|
|
14
|
+
Uses DI container mounted at app.state.container to create a UoW per request.
|
|
15
|
+
Assumes container.uow is a provider returning SqlAlchemyUnitOfWork(session_factory).
|
|
16
|
+
"""
|
|
17
|
+
container = getattr(request.app.state, "container", None)
|
|
18
|
+
if container is None or not hasattr(container, "uow"):
|
|
19
|
+
raise RuntimeError("Container with 'uow' provider not configured on app.state.container")
|
|
20
|
+
|
|
21
|
+
uow = container.uow()
|
|
22
|
+
with uow as _uow:
|
|
23
|
+
yield _uow
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_async_uow(request: Request) -> AsyncIterator[AsyncUnitOfWork]:
|
|
27
|
+
container = getattr(request.app.state, "container", None)
|
|
28
|
+
if container is None or not hasattr(container, "async_uow"):
|
|
29
|
+
raise RuntimeError("Container.async_uow provider not configured")
|
|
30
|
+
uow: AsyncSqlAlchemyUnitOfWork = container.async_uow()
|
|
31
|
+
async with uow as _uow:
|
|
32
|
+
yield _uow
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional
|
|
3
4
|
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
from fastapi import FastAPI, Request
|
|
6
|
-
from fastapi
|
|
7
|
+
from fastapi import HTTPException as FastAPIHTTPException
|
|
7
8
|
from fastapi.exceptions import RequestValidationError
|
|
9
|
+
from fastapi.responses import JSONResponse
|
|
8
10
|
from pydantic import ValidationError
|
|
9
|
-
from fastapi import HTTPException as FastAPIHTTPException
|
|
10
11
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
11
12
|
|
|
12
13
|
from .exceptions import BaseHttpException
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
# ---- Helpers ---------------------------------------------------------------
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
def _ensure_trace_id(request: Request) -> str:
|
|
18
19
|
rid = request.headers.get("X-Request-Id") or request.headers.get("X-Trace-Id")
|
|
19
20
|
return rid or str(uuid4())
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _json_response(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
request: Request,
|
|
25
|
+
status_code: int,
|
|
26
|
+
*,
|
|
27
|
+
detail: Any,
|
|
28
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
29
|
+
trace_id: Optional[str] = None,
|
|
30
|
+
extra: Optional[Mapping[str, Any]] = None,
|
|
30
31
|
) -> JSONResponse:
|
|
31
32
|
payload: dict[str, Any] = {"detail": detail}
|
|
32
33
|
if extra:
|
|
@@ -44,11 +45,12 @@ def _json_response(
|
|
|
44
45
|
|
|
45
46
|
# ---- Public Installer ------------------------------------------------------
|
|
46
47
|
|
|
48
|
+
|
|
47
49
|
def install_exception_handlers(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
app: FastAPI,
|
|
51
|
+
*,
|
|
52
|
+
logger: Any = None,
|
|
53
|
+
expose_server_errors: bool = False,
|
|
52
54
|
) -> None:
|
|
53
55
|
@app.exception_handler(BaseHttpException)
|
|
54
56
|
async def _handle_base_http_exception(request: Request, exc: BaseHttpException):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Any, Iterable
|
|
2
|
+
|
|
2
3
|
from fastapi import HTTPException, status
|
|
3
4
|
|
|
4
5
|
|
|
@@ -77,5 +78,3 @@ class UnprocessableEntityException(BaseHttpException):
|
|
|
77
78
|
class LogicalValidationException(UnprocessableEntityException):
|
|
78
79
|
def __init__(self, detail: str, loc: Iterable[Any] | None = None, type_: str = "logical_error"):
|
|
79
80
|
super().__init__(detail=detail, loc=loc, type_=type_)
|
|
80
|
-
|
|
81
|
-
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .access_log import AccessLogMiddleware
|
|
2
2
|
from .add_request_context import AddRequestContextMiddleware
|
|
3
|
-
from .
|
|
3
|
+
from .authentication import AuthenticationMiddleware
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from typing import Callable
|
|
3
|
+
|
|
3
4
|
from fastapi import Request
|
|
4
5
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
6
|
|
|
@@ -7,6 +8,7 @@ from nlbone.config.logging import get_logger
|
|
|
7
8
|
|
|
8
9
|
logger = get_logger(__name__)
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
class AccessLogMiddleware(BaseHTTPMiddleware):
|
|
11
13
|
async def dispatch(self, request: Request, call_next: Callable):
|
|
12
14
|
start = time.perf_counter()
|
|
@@ -20,11 +22,13 @@ class AccessLogMiddleware(BaseHTTPMiddleware):
|
|
|
20
22
|
raise
|
|
21
23
|
finally:
|
|
22
24
|
dur_ms = int((time.perf_counter() - start) * 1000)
|
|
23
|
-
logger.info(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
logger.info(
|
|
26
|
+
{
|
|
27
|
+
"event": "access",
|
|
28
|
+
"method": request.method,
|
|
29
|
+
"path": request.url.path,
|
|
30
|
+
"status": status_code,
|
|
31
|
+
"duration_ms": dur_ms,
|
|
32
|
+
"query": request.url.query,
|
|
33
|
+
}
|
|
34
|
+
)
|
|
@@ -1,52 +1,55 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from starlette.
|
|
7
|
-
from starlette.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from nlbone.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from starlette.datastructures import Headers
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
|
|
10
|
+
from nlbone.config.settings import get_settings
|
|
11
|
+
from nlbone.utils.context import bind_context, request_ctx, request_id_ctx, reset_context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def mock_request(user=None, token: Optional[str] = None):
|
|
15
|
+
request = Request(
|
|
16
|
+
{
|
|
17
|
+
"type": "http",
|
|
18
|
+
"headers": Headers({"User-Agent": "Testing-Agent"}).raw,
|
|
19
|
+
"client": {"host": "192.168.1.1", "port": 80},
|
|
20
|
+
"method": "GET",
|
|
21
|
+
"path": "/__test__",
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
request.state.user = user
|
|
25
|
+
request.state.token = token
|
|
26
|
+
return request
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def current_request() -> Optional[Request]:
|
|
30
|
+
req = request_ctx.get()
|
|
31
|
+
if get_settings().ENV == "local" and req is None:
|
|
32
|
+
return mock_request()
|
|
33
|
+
return req
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def current_request_id() -> Optional[str]:
|
|
37
|
+
return request_id_ctx.get()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AddRequestContextMiddleware(BaseHTTPMiddleware):
|
|
41
|
+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
|
|
42
|
+
incoming_req_id = request.headers.get("X-Request-ID")
|
|
43
|
+
req_id = incoming_req_id or str(uuid4())
|
|
44
|
+
|
|
45
|
+
user_id = getattr(getattr(request, "state", None), "user_id", None) or request.headers.get("X-User-Id")
|
|
46
|
+
ip = request.client.host if request.client else None
|
|
47
|
+
ua = request.headers.get("user-agent")
|
|
48
|
+
|
|
49
|
+
tokens = bind_context(request=request, request_id=req_id, user_id=user_id, ip=ip, user_agent=ua)
|
|
50
|
+
try:
|
|
51
|
+
response = await call_next(request)
|
|
52
|
+
response.headers.setdefault("X-Request-ID", req_id)
|
|
53
|
+
return response
|
|
54
|
+
finally:
|
|
55
|
+
reset_context(tokens)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Callable, Optional, Union
|
|
2
|
+
|
|
2
3
|
from fastapi import Request
|
|
3
4
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
4
5
|
|
|
5
6
|
try:
|
|
6
7
|
from dependency_injector import providers
|
|
8
|
+
|
|
7
9
|
ProviderType = providers.Provider # type: ignore
|
|
8
10
|
except Exception:
|
|
9
11
|
ProviderType = object
|
|
@@ -14,6 +16,7 @@ from nlbone.core.ports.auth import AuthService
|
|
|
14
16
|
def _to_factory(auth: Union[AuthService, Callable[[], AuthService], ProviderType]):
|
|
15
17
|
try:
|
|
16
18
|
from dependency_injector import providers as _p # type: ignore
|
|
19
|
+
|
|
17
20
|
if isinstance(auth, _p.Provider):
|
|
18
21
|
return auth
|
|
19
22
|
except Exception:
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from typing import Generic, List, Optional, TypeVar
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from fastapi import Depends
|
|
4
|
+
from pydantic import BaseModel
|
|
4
5
|
|
|
5
|
-
from .offset_base import
|
|
6
|
+
from .offset_base import PaginateRequest, PaginateResponse
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def get_pagination(
|
|
9
|
-
req: PaginateRequest = Depends(PaginateRequest)
|
|
10
|
-
) -> PaginateRequest:
|
|
9
|
+
def get_pagination(req: PaginateRequest = Depends(PaginateRequest)) -> PaginateRequest:
|
|
11
10
|
return req
|
|
12
11
|
|
|
13
12
|
|
|
@@ -3,7 +3,6 @@ from math import ceil
|
|
|
3
3
|
from typing import Any, Optional
|
|
4
4
|
|
|
5
5
|
from fastapi import Query
|
|
6
|
-
from sqlalchemy import asc, desc
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class PaginateRequest:
|
|
@@ -76,7 +75,6 @@ class PaginateRequest:
|
|
|
76
75
|
return filters_dict
|
|
77
76
|
|
|
78
77
|
|
|
79
|
-
|
|
80
78
|
class PaginateResponse:
|
|
81
79
|
"""
|
|
82
80
|
Lightweight response shaper. If total_count is None → returns just items.
|
nlbone/interfaces/cli/init_db.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
|
|
2
3
|
import anyio
|
|
3
4
|
|
|
4
5
|
from nlbone.adapters.db.sqlalchemy.schema import init_db_async, init_db_sync
|
|
5
6
|
|
|
7
|
+
|
|
6
8
|
def main() -> None:
|
|
7
9
|
parser = argparse.ArgumentParser(description="Initialize database schema (create_all).")
|
|
8
10
|
parser.add_argument("--async", dest="use_async", action="store_true", help="Use AsyncEngine")
|
|
@@ -13,5 +15,6 @@ def main() -> None:
|
|
|
13
15
|
else:
|
|
14
16
|
init_db_sync()
|
|
15
17
|
|
|
18
|
+
|
|
16
19
|
if __name__ == "__main__":
|
|
17
20
|
main()
|