nlbone 0.1.37__tar.gz → 0.2.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.
- {nlbone-0.1.37 → nlbone-0.2.0}/PKG-INFO +1 -1
- {nlbone-0.1.37 → nlbone-0.2.0}/pyproject.toml +1 -1
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/auth/keycloak.py +1 -1
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/query_builder.py +12 -2
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/config/settings.py +2 -2
- nlbone-0.2.0/src/nlbone/interfaces/api/dependencies/auth.py +48 -0
- nlbone-0.2.0/src/nlbone/interfaces/api/exceptions.py +67 -0
- nlbone-0.2.0/src/nlbone/interfaces/api/pagination/__init__.py +9 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/pagination/offset_base.py +1 -24
- nlbone-0.1.37/src/nlbone/interfaces/api/exceptions.py +0 -8
- nlbone-0.1.37/src/nlbone/interfaces/api/pagination/__init__.py +0 -1
- {nlbone-0.1.37 → nlbone-0.2.0}/.gitignore +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/LICENSE +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/README.md +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/auth/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/memory.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/postgres.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/base.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/engine.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/query/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/query/builder.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/query/coercion.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/query/filters.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/query/ordering.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/query/types.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/db/sqlalchemy/schema.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/http_clients/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/http_clients/email_gateway.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/http_clients/uploadchi.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/http_clients/uploadchi_async.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/messaging/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/adapters/messaging/redis.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/config/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/config/logging.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/container.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/application/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/application/services/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/application/services.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/application/use_cases/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/application/use_cases/register_user.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/domain/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/domain/events.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/domain/models.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/ports/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/ports/auth.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/ports/files.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/ports/messaging.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/core/ports/repo.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/routers.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/api/schemas.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/cli/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/cli/init_db.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/cli/main.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/jobs/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/types.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/utils/__init__.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/utils/context.py +0 -0
- {nlbone-0.1.37 → nlbone-0.2.0}/src/nlbone/utils/time.py +0 -0
|
@@ -11,7 +11,7 @@ class KeycloakAuthService(AuthService):
|
|
|
11
11
|
server_url=s.KEYCLOAK_SERVER_URL.__str__(),
|
|
12
12
|
client_id=s.KEYCLOAK_CLIENT_ID,
|
|
13
13
|
realm_name=s.KEYCLOAK_REALM_NAME,
|
|
14
|
-
client_secret_key=s.KEYCLOAK_CLIENT_SECRET.
|
|
14
|
+
client_secret_key=s.KEYCLOAK_CLIENT_SECRET.get_secret_value().strip(),
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
def has_access(self, token, permissions):
|
|
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session, Query
|
|
|
6
6
|
from sqlalchemy.dialects.postgresql import ENUM as PGEnum
|
|
7
7
|
|
|
8
8
|
from nlbone.interfaces.api.exceptions import UnprocessableEntityException
|
|
9
|
-
from nlbone.interfaces.api.pagination import PaginateRequest
|
|
9
|
+
from nlbone.interfaces.api.pagination import PaginateRequest, PaginateResponse
|
|
10
10
|
|
|
11
11
|
NULL_SENTINELS = {"None", "null", ""}
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ class _InvalidEnum(Exception):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def _apply_order(pagination, entity, query):
|
|
18
|
+
def _apply_order(pagination: PaginateRequest, entity, query):
|
|
19
19
|
if pagination.sort:
|
|
20
20
|
order_clauses = []
|
|
21
21
|
for sort in pagination.sort:
|
|
@@ -177,3 +177,13 @@ def apply_pagination(pagination: PaginateRequest, entity, session: Session, limi
|
|
|
177
177
|
if limit:
|
|
178
178
|
query = query.limit(pagination.limit).offset(pagination.offset)
|
|
179
179
|
return query
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_paginated_response(pagination: PaginateRequest, entity, session: Session, with_count=True) -> PaginateResponse:
|
|
183
|
+
query = apply_pagination(pagination, entity, session, not with_count)
|
|
184
|
+
total_count = None
|
|
185
|
+
if with_count:
|
|
186
|
+
total_count = query.count()
|
|
187
|
+
query = query.limit(pagination.limit).offset(pagination.offset)
|
|
188
|
+
return PaginateResponse(total_count=total_count, data=query.all(), limit=pagination.limit,
|
|
189
|
+
offset=pagination.offset).to_dict()
|
|
@@ -15,8 +15,8 @@ def _guess_env_file() -> str | None:
|
|
|
15
15
|
if cwd_env.exists():
|
|
16
16
|
return str(cwd_env)
|
|
17
17
|
|
|
18
|
-
for i in range(
|
|
19
|
-
p = Path(
|
|
18
|
+
for i in range(0, 8):
|
|
19
|
+
p = Path.cwd().resolve().parents[i]
|
|
20
20
|
f = p / ".env"
|
|
21
21
|
if f.exists():
|
|
22
22
|
return str(f)
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
def client_has_access(*, permissions=None):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
@functools.wraps(func)
|
|
11
|
+
def wrapper(*args, **kwargs):
|
|
12
|
+
request = current_request()
|
|
13
|
+
if not KeycloakAuthService().client_has_access(request.state.token, permissions=permissions):
|
|
14
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
15
|
+
|
|
16
|
+
return func(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
return wrapper
|
|
19
|
+
return decorator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def user_authenticated(func):
|
|
24
|
+
@functools.wraps(func)
|
|
25
|
+
async def wrapper(*args, **kwargs):
|
|
26
|
+
request = current_request()
|
|
27
|
+
if not request.state.user_id:
|
|
28
|
+
raise UnauthorizedException()
|
|
29
|
+
return await func(*args, **kwargs)
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_access(*, permissions=None):
|
|
35
|
+
def decorator(func):
|
|
36
|
+
@functools.wraps(func)
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
38
|
+
request = current_request()
|
|
39
|
+
if not request.state.user_id:
|
|
40
|
+
raise UnauthorizedException()
|
|
41
|
+
if not KeycloakAuthService().has_access(request.state.token, permissions=permissions):
|
|
42
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
43
|
+
|
|
44
|
+
return func(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
return wrapper
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Any, Iterable
|
|
2
|
+
from fastapi import HTTPException, status
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _error_entry(loc: Iterable[Any] | None, msg: str, type_: str) -> dict:
|
|
6
|
+
return {
|
|
7
|
+
"loc": list(loc) if loc else [],
|
|
8
|
+
"msg": msg,
|
|
9
|
+
"type": type_,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _errors(loc: Iterable[Any] | None, msg: str, type_: str) -> list[dict]:
|
|
14
|
+
return [_error_entry(loc, msg, type_)]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BadRequestException(HTTPException):
|
|
18
|
+
def __init__(self, msg: str):
|
|
19
|
+
super().__init__(
|
|
20
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
21
|
+
detail=msg,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UnauthorizedException(HTTPException):
|
|
26
|
+
def __init__(self, msg: str = "unauthorized"):
|
|
27
|
+
super().__init__(
|
|
28
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
29
|
+
detail=msg,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ForbiddenException(HTTPException):
|
|
34
|
+
def __init__(self, msg: str = "forbidden"):
|
|
35
|
+
super().__init__(
|
|
36
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
37
|
+
detail=msg,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotFoundException(HTTPException):
|
|
42
|
+
def __init__(self, msg: str = "not found"):
|
|
43
|
+
super().__init__(
|
|
44
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
45
|
+
detail=msg,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConflictException(HTTPException):
|
|
50
|
+
def __init__(self, msg: str = "conflict"):
|
|
51
|
+
super().__init__(
|
|
52
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
53
|
+
detail=msg,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UnprocessableEntityException(HTTPException):
|
|
58
|
+
def __init__(self, msg: str, loc: Iterable[Any] | None = None, type_: str = "unprocessable_entity"):
|
|
59
|
+
super().__init__(
|
|
60
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
61
|
+
detail=_errors(loc, msg, type_),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LogicalValidationException(UnprocessableEntityException):
|
|
66
|
+
def __init__(self, msg: str, loc: Iterable[Any] | None = None, type_: str = "logical_error"):
|
|
67
|
+
super().__init__(msg=msg, loc=loc, type_=type_)
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
# src/nlbone/interfaces/api/pagination.py
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
1
|
import json
|
|
5
2
|
from math import ceil
|
|
6
3
|
from typing import Any, Optional
|
|
@@ -21,7 +18,7 @@ class PaginateRequest:
|
|
|
21
18
|
limit: int = 10,
|
|
22
19
|
offset: int = 0,
|
|
23
20
|
sort: Optional[str] = None,
|
|
24
|
-
filters: Optional[str] = Query(None, description="e.g. title:abc
|
|
21
|
+
filters: Optional[str] = Query(None, description="e.g. title:abc"),
|
|
25
22
|
) -> None:
|
|
26
23
|
self.limit = max(0, limit)
|
|
27
24
|
self.offset = max(0, offset)
|
|
@@ -78,26 +75,6 @@ class PaginateRequest:
|
|
|
78
75
|
filters_dict[key] = value_cast
|
|
79
76
|
return filters_dict
|
|
80
77
|
|
|
81
|
-
# Helpers for SQLAlchemy usage:
|
|
82
|
-
def build_order_by(self, model) -> list[Any]:
|
|
83
|
-
"""
|
|
84
|
-
Translate parsed sort to SQLAlchemy order_by list.
|
|
85
|
-
Example: model.created_at.desc(), model.id.asc()
|
|
86
|
-
"""
|
|
87
|
-
items: list[Any] = []
|
|
88
|
-
for item in self.sort:
|
|
89
|
-
field = getattr(model, item["field"], None)
|
|
90
|
-
if field is None:
|
|
91
|
-
continue
|
|
92
|
-
items.append(desc(field) if item["order"] == "desc" else asc(field))
|
|
93
|
-
return items
|
|
94
|
-
|
|
95
|
-
def filter_kwargs(self) -> dict[str, Any]:
|
|
96
|
-
"""
|
|
97
|
-
Return simple equality filters (for model.filter_by(**kwargs)).
|
|
98
|
-
For advanced ops, extend this to parse operators (gt, lt, like, in, etc.).
|
|
99
|
-
"""
|
|
100
|
-
return {k: v for k, v in self.filters.items() if k != "$"}
|
|
101
78
|
|
|
102
79
|
|
|
103
80
|
class PaginateResponse:
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
from fastapi import HTTPException, status
|
|
2
|
-
|
|
3
|
-
class UnprocessableEntityException(HTTPException):
|
|
4
|
-
def __init__(self, detail: str, loc: list[str] | None = None):
|
|
5
|
-
super().__init__(
|
|
6
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
7
|
-
detail={"msg": detail, "loc": loc or []},
|
|
8
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .offset_base import PaginateResponse, PaginateRequest
|
|
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
|
|
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
|
|
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
|
|
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
|