nlbone 0.1.37__py3-none-any.whl → 0.2.0__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.
@@ -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.__str__(),
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()
nlbone/config/settings.py CHANGED
@@ -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(1, 8):
19
- p = Path(__file__).resolve().parents[i]
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
+
@@ -1,8 +1,67 @@
1
+ from typing import Any, Iterable
1
2
  from fastapi import HTTPException, status
2
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
+
3
57
  class UnprocessableEntityException(HTTPException):
4
- def __init__(self, detail: str, loc: list[str] | None = None):
58
+ def __init__(self, msg: str, loc: Iterable[Any] | None = None, type_: str = "unprocessable_entity"):
5
59
  super().__init__(
6
60
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
7
- detail={"msg": detail, "loc": loc or []},
61
+ detail=_errors(loc, msg, type_),
8
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 +1,9 @@
1
- from .offset_base import PaginateResponse, PaginateRequest
1
+ from fastapi import Depends
2
+
3
+ from .offset_base import PaginateResponse, PaginateRequest
4
+
5
+
6
+ def get_pagination(
7
+ req: PaginateRequest = Depends(PaginateRequest)
8
+ ) -> PaginateRequest:
9
+ return req
@@ -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 or JSON"),
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.1.37
3
+ Version: 0.2.0
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
@@ -3,11 +3,11 @@ 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
6
- nlbone/adapters/auth/keycloak.py,sha256=kTjndbDtoCfDohJi59kbXb8JhoEXo8Na_OzraaOVotw,2434
6
+ nlbone/adapters/auth/keycloak.py,sha256=lPmRmCwBuDj9fUJsGMUCOuk_MsuLBQYBrO3QvBBaV8I,2451
7
7
  nlbone/adapters/db/__init__.py,sha256=aHur2GuykZd26RpEmIbkAfflkksZuKWAlYLJMIYotQE,144
8
8
  nlbone/adapters/db/memory.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  nlbone/adapters/db/postgres.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- nlbone/adapters/db/query_builder.py,sha256=qL2Ppe39X7pnKpHfWZZanxfEKqQW3grZv5bQLS52mfU,6066
10
+ nlbone/adapters/db/query_builder.py,sha256=xrhBiWRSsyLHzGft0weHwqRPOiP2i6ajqcxJOruHOF4,6606
11
11
  nlbone/adapters/db/sqlalchemy/__init__.py,sha256=REK4P8SF6kvDNYgkZIbSowhHPivyFVnMCYGfYT1dlxk,158
12
12
  nlbone/adapters/db/sqlalchemy/base.py,sha256=-5FnCMKETJ2xykhViHQuNBdbRMaxuieuatgiEl4Lllw,73
13
13
  nlbone/adapters/db/sqlalchemy/engine.py,sha256=m3nVY7KU12ksM3vK9fEgYEobrz9L8hfsgjI8cSNtBMY,3030
@@ -26,7 +26,7 @@ nlbone/adapters/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
26
26
  nlbone/adapters/messaging/redis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  nlbone/config/logging.py,sha256=68WRQejEpL6eHEY_cOgdlOjndKc-RWth0n4YmXnceC8,5041
29
- nlbone/config/settings.py,sha256=i89s5Jbb3qzW8vYXjxrm__bx0OKhFvEfqltwE95lVMc,3278
29
+ nlbone/config/settings.py,sha256=2PEGXJwmhjHK9wuX2AKCfPZWz3F1dvJ5ig4Sad7jqlc,3274
30
30
  nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  nlbone/core/application/services.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -43,17 +43,18 @@ nlbone/core/ports/messaging.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
43
43
  nlbone/core/ports/repo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  nlbone/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  nlbone/interfaces/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
- nlbone/interfaces/api/exceptions.py,sha256=OczR1FND2nbCngwbfgaPrgDbDaz4Pc47mFSHgtL7tW8,313
46
+ nlbone/interfaces/api/exceptions.py,sha256=mw0nZHO2oKml3-d7cdSBTQeVGKFQCjLn_TJy6rxcrU0,1901
47
47
  nlbone/interfaces/api/routers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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
+ nlbone/interfaces/api/dependencies/auth.py,sha256=XwzKga9GkcvCKUg37zEnVw8m30MbR-4wh1CTeLG2jH8,1445
50
51
  nlbone/interfaces/api/dependencies/db.py,sha256=IqDVq1lcCCxd22FBUg523lVANM_j71BYAQtsbrHc4M8,465
51
52
  nlbone/interfaces/api/middleware/__init__.py,sha256=Xcxg9Oy8uToPXaTSdBTKhst-hZwsaIEhqxx4mmo1bZI,157
52
53
  nlbone/interfaces/api/middleware/access_log.py,sha256=dEjk_m4fyQ72S2xLzDydDoaw2F9Tvmfl_acat1YThE0,972
53
54
  nlbone/interfaces/api/middleware/add_request_context.py,sha256=i8EV4BvZyrBcNfU-uTkybr7J7ypYvJq8mXSntM6oel4,1816
54
55
  nlbone/interfaces/api/middleware/authentication.py,sha256=-jfV-Ry04qsFd1cJh7KGoGpfnUKIogPF2fK42ZTFgBk,1806
55
- nlbone/interfaces/api/pagination/__init__.py,sha256=fdTqy5efdYIcUWbK1mVPQMieGd3HV1lZ8vmIhhsuUKs,58
56
- nlbone/interfaces/api/pagination/offset_base.py,sha256=W0SJ0rHLMIqoyiPEAjnoKqY57LBXEyT5J-_5bS8r-NY,4535
56
+ nlbone/interfaces/api/pagination/__init__.py,sha256=dduTd8cOUnKOipqpbsi5gANXKsD3uUttdzu4S5ObS_U,203
57
+ nlbone/interfaces/api/pagination/offset_base.py,sha256=2MINXX4wXoynOTPgn28lp_UQufBUluLh_qjF-5AYFxk,3655
57
58
  nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
59
  nlbone/interfaces/cli/init_db.py,sha256=6Q05gwcPa6ywwz3Dzi0hiV8bycg0zU_3eWGsL6VNVRk,479
59
60
  nlbone/interfaces/cli/main.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -62,7 +63,7 @@ nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
62
63
  nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
64
  nlbone/utils/context.py,sha256=AUiN1jM0ebNMopZQoJSqWTfUHuVrp-HV8x6g7QsbEJ8,1601
64
65
  nlbone/utils/time.py,sha256=dC0ucyAmHdNf3wpA_JPinl2VJRubWqx2vcRpJsT3-0k,102
65
- nlbone-0.1.37.dist-info/METADATA,sha256=X4wipUdAO0zS_LfafPT1U3HGmJ8dlxkgLX-iyH-y7Mg,2299
66
- nlbone-0.1.37.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
67
- nlbone-0.1.37.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- nlbone-0.1.37.dist-info/RECORD,,
66
+ nlbone-0.2.0.dist-info/METADATA,sha256=VVTESRwO-HZwMfXeb_EszCLS52Jv7y-zXWC54Wtmt8w,2298
67
+ nlbone-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
68
+ nlbone-0.2.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
+ nlbone-0.2.0.dist-info/RECORD,,