nlbone 0.4.0__py3-none-any.whl → 0.4.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.
- nlbone/adapters/auth/__init__.py +1 -1
- nlbone/adapters/auth/keycloak.py +6 -1
- nlbone/adapters/db/__init__.py +4 -3
- nlbone/adapters/db/postgres/__init__.py +4 -0
- nlbone/adapters/db/postgres/audit.py +148 -0
- nlbone/adapters/db/{sqlalchemy → postgres}/base.py +0 -2
- nlbone/adapters/db/{sqlalchemy → postgres}/engine.py +4 -3
- nlbone/adapters/db/{sqlalchemy → postgres}/query_builder.py +27 -11
- nlbone/adapters/db/{sqlalchemy → postgres}/repository.py +4 -2
- nlbone/adapters/db/{sqlalchemy → postgres}/schema.py +7 -7
- nlbone/adapters/db/{sqlalchemy → postgres}/uow.py +3 -2
- nlbone/adapters/db/redis/client.py +22 -0
- nlbone/adapters/http_clients/uploadchi.py +27 -11
- nlbone/adapters/http_clients/uploadchi_async.py +29 -7
- nlbone/adapters/messaging/event_bus.py +3 -0
- nlbone/adapters/percolation/__init__.py +1 -0
- nlbone/adapters/percolation/connection.py +12 -0
- nlbone/config/logging.py +76 -117
- nlbone/config/settings.py +20 -24
- nlbone/container.py +6 -5
- nlbone/core/application/base_worker.py +36 -0
- nlbone/core/application/events.py +5 -1
- nlbone/core/application/use_case.py +3 -1
- nlbone/core/domain/base.py +4 -0
- nlbone/core/domain/models.py +38 -0
- nlbone/core/ports/__init__.py +3 -3
- nlbone/core/ports/auth.py +1 -0
- nlbone/core/ports/event_bus.py +3 -0
- nlbone/core/ports/files.py +26 -5
- nlbone/core/ports/repo.py +5 -2
- nlbone/core/ports/uow.py +3 -0
- nlbone/interfaces/api/dependencies/__init__.py +11 -3
- nlbone/interfaces/api/dependencies/async_auth.py +61 -0
- nlbone/interfaces/api/dependencies/auth.py +5 -3
- nlbone/interfaces/api/dependencies/db.py +5 -3
- nlbone/interfaces/api/dependencies/uow.py +3 -2
- 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 +24 -13
- nlbone/interfaces/cli/main.py +29 -0
- nlbone/utils/context.py +14 -4
- nlbone/utils/redactor.py +32 -0
- nlbone/utils/time.py +41 -2
- {nlbone-0.4.0.dist-info → nlbone-0.4.2.dist-info}/METADATA +5 -9
- nlbone-0.4.2.dist-info/RECORD +78 -0
- nlbone-0.4.2.dist-info/entry_points.txt +2 -0
- nlbone/adapters/db/postgres.py +0 -0
- nlbone/adapters/db/sqlalchemy/__init__.py +0 -4
- nlbone-0.4.0.dist-info/RECORD +0 -71
- /nlbone/adapters/db/{memory.py → redis/__init__.py} +0 -0
- {nlbone-0.4.0.dist-info → nlbone-0.4.2.dist-info}/WHEEL +0 -0
- {nlbone-0.4.0.dist-info → nlbone-0.4.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,16 +1,18 @@
|
|
|
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
|
-
from nlbone.adapters.db.
|
|
8
|
+
from nlbone.adapters.db.postgres.engine import async_session, sync_session
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
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
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from collections.abc import Iterator
|
|
3
4
|
from typing import AsyncIterator
|
|
4
5
|
|
|
5
6
|
from fastapi import Request
|
|
6
7
|
|
|
7
|
-
from nlbone.adapters.db.
|
|
8
|
-
from nlbone.core.ports.uow import
|
|
8
|
+
from nlbone.adapters.db.postgres import AsyncSqlAlchemyUnitOfWork
|
|
9
|
+
from nlbone.core.ports.uow import AsyncUnitOfWork, UnitOfWork
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def get_uow(request: Request) -> Iterator[UnitOfWork]:
|
|
@@ -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,17 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import anyio
|
|
1
|
+
import typer
|
|
3
2
|
|
|
4
|
-
from nlbone.adapters.db
|
|
3
|
+
from nlbone.adapters.db import init_sync_engine, Base, sync_ping
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
parser = argparse.ArgumentParser(description="Initialize database schema (create_all).")
|
|
8
|
-
parser.add_argument("--async", dest="use_async", action="store_true", help="Use AsyncEngine")
|
|
9
|
-
args = parser.parse_args()
|
|
5
|
+
init_db_command = typer.Typer(help="Database utilities")
|
|
10
6
|
|
|
11
|
-
if args.use_async:
|
|
12
|
-
anyio.run(init_db_async)
|
|
13
|
-
else:
|
|
14
|
-
init_db_sync()
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
@init_db_command.command("init")
|
|
9
|
+
def init_db(drop: bool = typer.Option(False, "--drop", help="Drop all tables before create")):
|
|
10
|
+
"""Create (and optionally drop) DB schema."""
|
|
11
|
+
engine = init_sync_engine()
|
|
12
|
+
if drop:
|
|
13
|
+
Base.metadata.drop_all(bind=engine)
|
|
14
|
+
Base.metadata.create_all(bind=engine)
|
|
15
|
+
typer.echo("✅ DB schema initialized.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@init_db_command.command("ping")
|
|
19
|
+
def ping():
|
|
20
|
+
"""Health check."""
|
|
21
|
+
sync_ping()
|
|
22
|
+
typer.echo("✅ DB connection OK")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@init_db_command.command("migrate")
|
|
26
|
+
def migrate():
|
|
27
|
+
"""Placeholder for migration trigger (Alembic, etc.)."""
|
|
28
|
+
typer.echo("ℹ️ Hook your migration tool here.")
|
nlbone/interfaces/cli/main.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from nlbone.adapters.db import init_sync_engine
|
|
5
|
+
from nlbone.config.settings import get_settings
|
|
6
|
+
from nlbone.interfaces.cli.init_db import init_db_command
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="NLBone CLI")
|
|
9
|
+
|
|
10
|
+
app.add_typer(init_db_command, name="db")
|
|
11
|
+
|
|
12
|
+
@app.callback()
|
|
13
|
+
def common(
|
|
14
|
+
env_file: Optional[str] = typer.Option(
|
|
15
|
+
None, "--env-file", "-e",
|
|
16
|
+
help="Path to .env file. In prod omit this."
|
|
17
|
+
),
|
|
18
|
+
debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
|
|
19
|
+
):
|
|
20
|
+
settings = get_settings(env_file=env_file)
|
|
21
|
+
if debug:
|
|
22
|
+
pass
|
|
23
|
+
init_sync_engine(echo=settings.DEBUG)
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
app()
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|
nlbone/utils/context.py
CHANGED
|
@@ -7,9 +7,15 @@ user_id_ctx: ContextVar[str | None] = ContextVar("user_id", default=None)
|
|
|
7
7
|
ip_ctx: ContextVar[str | None] = ContextVar("ip", default=None)
|
|
8
8
|
user_agent_ctx: ContextVar[str | None] = ContextVar("user_agent", default=None)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
def bind_context(
|
|
12
|
+
*,
|
|
13
|
+
request: Any | None = None,
|
|
14
|
+
request_id: str | None = None,
|
|
15
|
+
user_id: str | None = None,
|
|
16
|
+
ip: str | None = None,
|
|
17
|
+
user_agent: str | None = None,
|
|
18
|
+
) -> dict[ContextVar, Any]:
|
|
13
19
|
tokens: dict[ContextVar, Any] = {}
|
|
14
20
|
if request is not None:
|
|
15
21
|
tokens[request_ctx] = request_ctx.set(request)
|
|
@@ -23,20 +29,24 @@ def bind_context(*, request: Any | None = None, request_id: str | None = None,
|
|
|
23
29
|
tokens[user_agent_ctx] = user_agent_ctx.set(user_agent)
|
|
24
30
|
return tokens
|
|
25
31
|
|
|
32
|
+
|
|
26
33
|
def reset_context(tokens: dict[ContextVar, Any]):
|
|
27
34
|
for var, token in tokens.items():
|
|
28
35
|
var.reset(token)
|
|
29
36
|
|
|
37
|
+
|
|
30
38
|
def current_request():
|
|
31
39
|
return request_ctx.get()
|
|
32
40
|
|
|
41
|
+
|
|
33
42
|
def current_request_id() -> Optional[str]:
|
|
34
43
|
return request_id_ctx.get()
|
|
35
44
|
|
|
45
|
+
|
|
36
46
|
def current_context_dict() -> dict[str, Any]:
|
|
37
47
|
return {
|
|
38
48
|
"request_id": request_id_ctx.get(),
|
|
39
49
|
"user_id": user_id_ctx.get(),
|
|
40
50
|
"ip": ip_ctx.get(),
|
|
41
51
|
"user_agent": user_agent_ctx.get(),
|
|
42
|
-
}
|
|
52
|
+
}
|
nlbone/utils/redactor.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
SENSITIVE_KEYS = {"password", "token", "access_token", "refresh_token", "secret", "card_number", "cvv", "pan"}
|
|
7
|
+
|
|
8
|
+
class PiiRedactor(logging.Filter):
|
|
9
|
+
def _redact_in_obj(self, obj: Any):
|
|
10
|
+
if isinstance(obj, dict):
|
|
11
|
+
return {k: ("***" if k.lower() in SENSITIVE_KEYS else self._redact_in_obj(v)) for k, v in obj.items()}
|
|
12
|
+
if isinstance(obj, (list, tuple)):
|
|
13
|
+
return [self._redact_in_obj(v) for v in obj]
|
|
14
|
+
if isinstance(obj, str):
|
|
15
|
+
obj = re.sub(r"\b(\d{6})\d{6}(\d{4})\b", r"\1******\2", obj)
|
|
16
|
+
return obj
|
|
17
|
+
|
|
18
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
if isinstance(record.args, dict):
|
|
21
|
+
record.args = self._redact_in_obj(record.args)
|
|
22
|
+
if isinstance(record.msg, dict):
|
|
23
|
+
record.msg = self._redact_in_obj(record.msg)
|
|
24
|
+
elif isinstance(record.msg, str):
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(record.msg)
|
|
27
|
+
record.msg = json.dumps(self._redact_in_obj(data), ensure_ascii=False)
|
|
28
|
+
except Exception:
|
|
29
|
+
record.msg = self._redact_in_obj(record.msg)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return True
|
nlbone/utils/time.py
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
|
|
3
|
+
from dateutil import parser
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
def now() -> datetime:
|
|
5
|
-
return datetime.now(timezone.utc)
|
|
7
|
+
return datetime.now(timezone.utc)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TimeUtility:
|
|
11
|
+
@classmethod
|
|
12
|
+
def now(cls) -> datetime:
|
|
13
|
+
return datetime.now(timezone.utc)
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def minutes_left_from_now(cls, ts: str | datetime) -> int:
|
|
17
|
+
dt = parser.parse(ts) if isinstance(ts, str) else ts
|
|
18
|
+
if dt.tzinfo is None:
|
|
19
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
20
|
+
now = datetime.now(timezone.utc)
|
|
21
|
+
delta_sec = (dt - now).total_seconds()
|
|
22
|
+
return int(delta_sec // 60)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_datetime(cls, ts: str | datetime) -> datetime:
|
|
26
|
+
dt = parser.parse(ts) if isinstance(ts, str) else ts
|
|
27
|
+
if dt.tzinfo is None:
|
|
28
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
29
|
+
return dt
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def get_past_datetime(
|
|
33
|
+
cls, days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0
|
|
34
|
+
) -> datetime:
|
|
35
|
+
delta = timedelta(
|
|
36
|
+
days=days,
|
|
37
|
+
seconds=seconds,
|
|
38
|
+
microseconds=microseconds,
|
|
39
|
+
milliseconds=milliseconds,
|
|
40
|
+
minutes=minutes,
|
|
41
|
+
hours=hours,
|
|
42
|
+
weeks=weeks,
|
|
43
|
+
)
|
|
44
|
+
return cls.now() - delta
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nlbone
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -8,23 +8,19 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Requires-Dist: anyio>=4.0
|
|
10
10
|
Requires-Dist: dependency-injector>=4.48.1
|
|
11
|
+
Requires-Dist: elasticsearch==8.14.0
|
|
11
12
|
Requires-Dist: fastapi>=0.116
|
|
12
13
|
Requires-Dist: httpx>=0.27
|
|
13
14
|
Requires-Dist: psycopg>=3.2.9
|
|
14
15
|
Requires-Dist: pydantic-settings>=2.0
|
|
15
16
|
Requires-Dist: pydantic>=2.0
|
|
17
|
+
Requires-Dist: python-dateutil~=2.9.0.post0
|
|
16
18
|
Requires-Dist: python-keycloak==5.8.1
|
|
19
|
+
Requires-Dist: redis~=6.4.0
|
|
17
20
|
Requires-Dist: sqlalchemy>=2.0
|
|
18
21
|
Requires-Dist: starlette>=0.47
|
|
22
|
+
Requires-Dist: typer>=0.17.4
|
|
19
23
|
Requires-Dist: uvicorn>=0.35
|
|
20
|
-
Provides-Extra: dev
|
|
21
|
-
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
22
|
-
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
23
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
24
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
-
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
26
|
-
Requires-Dist: tomli; extra == 'dev'
|
|
27
|
-
Requires-Dist: twine; extra == 'dev'
|
|
28
24
|
Description-Content-Type: text/markdown
|
|
29
25
|
|
|
30
26
|
# nlbone
|