nlbone 0.1.38__py3-none-any.whl → 0.3.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.
@@ -1,2 +1,3 @@
1
1
  from .sqlalchemy.engine import init_async_engine, async_session, async_ping, init_sync_engine, sync_session, sync_ping
2
- from .sqlalchemy import *
2
+ from .sqlalchemy import get_paginated_response, apply_pagination
3
+ from .sqlalchemy.base import Base
@@ -1,4 +1 @@
1
- from .query import apply_pagination, apply_filters, apply_order
2
- from .base import Base
3
-
4
- __all__ = ["apply_pagination", "apply_filters", "apply_order", "Base"]
1
+ from .query_builder import get_paginated_response, apply_pagination
@@ -1,4 +1,7 @@
1
+ from typing import Union, Callable, Any, Optional, Type, Sequence
2
+
1
3
  from sqlalchemy import asc, desc, or_
4
+ from sqlalchemy.orm.interfaces import LoaderOption
2
5
  from sqlalchemy.sql.sqltypes import (
3
6
  String, Text, Integer, BigInteger, SmallInteger, Numeric, Float, Boolean, Enum as SAEnum
4
7
  )
@@ -6,7 +9,7 @@ from sqlalchemy.orm import Session, Query
6
9
  from sqlalchemy.dialects.postgresql import ENUM as PGEnum
7
10
 
8
11
  from nlbone.interfaces.api.exceptions import UnprocessableEntityException
9
- from nlbone.interfaces.api.pagination import PaginateRequest
12
+ from nlbone.interfaces.api.pagination import PaginateRequest, PaginateResponse
10
13
 
11
14
  NULL_SENTINELS = {"None", "null", ""}
12
15
 
@@ -15,7 +18,7 @@ class _InvalidEnum(Exception):
15
18
  pass
16
19
 
17
20
 
18
- def _apply_order(pagination, entity, query):
21
+ def _apply_order(pagination: PaginateRequest, entity, query):
19
22
  if pagination.sort:
20
23
  order_clauses = []
21
24
  for sort in pagination.sort:
@@ -170,10 +173,84 @@ def _apply_filters(pagination, entity, query):
170
173
  return query
171
174
 
172
175
 
173
- def apply_pagination(pagination: PaginateRequest, entity, session: Session, limit=True) -> Query:
174
- query = session.query(entity)
176
+ def apply_pagination(pagination: PaginateRequest, entity, session: Session, limit=True, query=None) -> Query:
177
+ if not query:
178
+ query = session.query(entity)
175
179
  query = _apply_filters(pagination, entity, query)
176
180
  query = _apply_order(pagination, entity, query)
177
181
  if limit:
178
182
  query = query.limit(pagination.limit).offset(pagination.offset)
179
183
  return query
184
+
185
+
186
+ OutputType = Union[type, Callable[[Any], Any], None]
187
+
188
+
189
+ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
190
+ """Serialize a single ORM item based on output_cls (Pydantic v1/v2 or custom mapper)."""
191
+ if output_cls is None:
192
+ return item
193
+
194
+ if callable(output_cls) and not isinstance(output_cls, type):
195
+ return output_cls(item)
196
+
197
+ if hasattr(output_cls, "model_validate"):
198
+ try:
199
+ model = output_cls.model_validate(item, from_attributes=True) # type: ignore[attr-defined]
200
+ if hasattr(model, "model_dump"):
201
+ return model.model_dump() # type: ignore[attr-defined]
202
+ return model
203
+ except Exception:
204
+ pass
205
+
206
+ if hasattr(output_cls, "from_orm"):
207
+ try:
208
+ model = output_cls.from_orm(item) # type: ignore[attr-defined]
209
+ if hasattr(model, "dict"):
210
+ return model.dict() # type: ignore[attr-defined]
211
+ return model
212
+ except Exception:
213
+ pass
214
+
215
+ try:
216
+ obj = output_cls(item) # type: ignore[call-arg]
217
+ try:
218
+ from dataclasses import is_dataclass, asdict
219
+ if is_dataclass(obj):
220
+ return asdict(obj)
221
+ except Exception:
222
+ pass
223
+ return obj
224
+ except Exception:
225
+ return item
226
+
227
+
228
+ def get_paginated_response(
229
+ pagination,
230
+ entity,
231
+ session: Session,
232
+ *,
233
+ with_count: bool = True,
234
+ output_cls: Optional[Type] = None,
235
+ eager_options: Optional[Sequence[LoaderOption]] = None,
236
+ ) -> dict:
237
+ # پایه‌ی کوئری
238
+ query = session.query(entity)
239
+ if eager_options:
240
+ query = query.options(*eager_options)
241
+
242
+ query = apply_pagination(pagination, entity, session, not with_count, query=query)
243
+
244
+ total_count = None
245
+ if with_count:
246
+ total_count = query.count()
247
+ query = query.limit(pagination.limit).offset(pagination.offset)
248
+
249
+ rows = query.all()
250
+
251
+ if output_cls is not None:
252
+ data = [output_cls.model_validate(r, from_attributes=True).model_dump() for r in rows]
253
+ else:
254
+ data = rows
255
+ return PaginateResponse(total_count=total_count, data=data, limit=pagination.limit,
256
+ 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
+
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Optional, Mapping
3
+ from uuid import uuid4
4
+
5
+ from fastapi import FastAPI, Request
6
+ from fastapi.responses import JSONResponse
7
+ from fastapi.exceptions import RequestValidationError
8
+ from pydantic import ValidationError
9
+ from fastapi import HTTPException as FastAPIHTTPException
10
+ from starlette.exceptions import HTTPException as StarletteHTTPException
11
+
12
+ from .exceptions import BaseHttpException
13
+
14
+
15
+ # ---- Helpers ---------------------------------------------------------------
16
+
17
+ def _ensure_trace_id(request: Request) -> str:
18
+ rid = request.headers.get("X-Request-Id") or request.headers.get("X-Trace-Id")
19
+ return rid or str(uuid4())
20
+
21
+
22
+ def _json_response(
23
+ request: Request,
24
+ status_code: int,
25
+ *,
26
+ detail: Any,
27
+ headers: Optional[Mapping[str, str]] = None,
28
+ trace_id: Optional[str] = None,
29
+ extra: Optional[Mapping[str, Any]] = None,
30
+ ) -> JSONResponse:
31
+ payload: dict[str, Any] = {"detail": detail}
32
+ if extra:
33
+ payload.update(extra)
34
+ tid = trace_id or _ensure_trace_id(request)
35
+
36
+ payload.setdefault("trace_id", tid)
37
+
38
+ base_headers = {"X-Trace-Id": tid}
39
+ if headers:
40
+ base_headers.update(headers)
41
+
42
+ return JSONResponse(status_code=status_code, content=payload, headers=base_headers)
43
+
44
+
45
+ # ---- Public Installer ------------------------------------------------------
46
+
47
+ def install_exception_handlers(
48
+ app: FastAPI,
49
+ *,
50
+ logger: Any = None,
51
+ expose_server_errors: bool = False,
52
+ ) -> None:
53
+ @app.exception_handler(BaseHttpException)
54
+ async def _handle_base_http_exception(request: Request, exc: BaseHttpException):
55
+ if logger:
56
+ logger.warning(
57
+ "http_error",
58
+ extra={"status": exc.status_code, "detail": exc.detail, "path": request.url.path},
59
+ )
60
+ return _json_response(request, exc.status_code, detail=exc.detail)
61
+
62
+ @app.exception_handler(FastAPIHTTPException)
63
+ async def _handle_fastapi_http_exception(request: Request, exc: FastAPIHTTPException):
64
+ if logger:
65
+ logger.warning(
66
+ "fastapi_http_error",
67
+ extra={"status": exc.status_code, "detail": exc.detail, "path": request.url.path},
68
+ )
69
+ return _json_response(request, exc.status_code, detail=exc.detail)
70
+
71
+ @app.exception_handler(StarletteHTTPException)
72
+ async def _handle_starlette_http_exception(request: Request, exc: StarletteHTTPException):
73
+ if logger:
74
+ logger.warning(
75
+ "starlette_http_error",
76
+ extra={"status": exc.status_code, "detail": exc.detail, "path": request.url.path},
77
+ )
78
+ return _json_response(request, exc.status_code, detail=exc.detail)
79
+
80
+ # 3) خطاهای اعتبارسنجی FastAPI (request body/query/path)
81
+ @app.exception_handler(RequestValidationError)
82
+ async def _handle_request_validation_error(request: Request, exc: RequestValidationError):
83
+ errors = exc.errors()
84
+ if logger:
85
+ logger.info(
86
+ "request_validation_error",
87
+ extra={"errors": errors, "path": request.url.path},
88
+ )
89
+ return _json_response(request, 422, detail=errors)
90
+
91
+ @app.exception_handler(ValidationError)
92
+ async def _handle_pydantic_validation_error(request: Request, exc: ValidationError):
93
+ errors = exc.errors()
94
+ if logger:
95
+ logger.info(
96
+ "pydantic_validation_error",
97
+ extra={"errors": errors, "path": request.url.path},
98
+ )
99
+ return _json_response(request, 422, detail=errors)
100
+
101
+ @app.exception_handler(Exception)
102
+ async def _handle_unexpected_exception(request: Request, exc: Exception):
103
+ tid = _ensure_trace_id(request)
104
+ if logger:
105
+ logger.exception("unhandled_exception", extra={"trace_id": tid, "path": request.url.path})
106
+ detail = str(exc) if expose_server_errors else "internal server error"
107
+ return _json_response(request, 500, detail=detail, trace_id=tid)
@@ -1,8 +1,81 @@
1
+ from typing import Any, Iterable
1
2
  from fastapi import HTTPException, status
2
3
 
3
- class UnprocessableEntityException(HTTPException):
4
- def __init__(self, detail: str, loc: list[str] | None = None):
4
+
5
+ def _error_entry(loc: Iterable[Any] | None, detail: str, type_: str) -> dict:
6
+ return {
7
+ "loc": list(loc) if loc else [],
8
+ "msg": detail,
9
+ "type": type_,
10
+ }
11
+
12
+
13
+ def _errors(loc: Iterable[Any] | None, detail: str, type_: str) -> list[dict]:
14
+ return [_error_entry(loc, detail, type_)]
15
+
16
+
17
+ class BaseHttpException(HTTPException):
18
+ pass
19
+
20
+
21
+ class BadRequestException(BaseHttpException):
22
+ def __init__(self, detail: str):
23
+ super().__init__(
24
+ status_code=status.HTTP_400_BAD_REQUEST,
25
+ detail=detail,
26
+ )
27
+
28
+
29
+ class UnauthorizedException(BaseHttpException):
30
+ def __init__(self, detail: str = "unauthorized"):
31
+ super().__init__(
32
+ status_code=status.HTTP_401_UNAUTHORIZED,
33
+ detail=detail,
34
+ )
35
+
36
+
37
+ class ForbiddenException(BaseHttpException):
38
+ def __init__(self, detail: str = "forbidden"):
39
+ super().__init__(
40
+ status_code=status.HTTP_403_FORBIDDEN,
41
+ detail=detail,
42
+ )
43
+
44
+
45
+ class NotFoundException(BaseHttpException):
46
+ def __init__(self, detail: str = "not found"):
47
+ super().__init__(
48
+ status_code=status.HTTP_404_NOT_FOUND,
49
+ detail=detail,
50
+ )
51
+
52
+
53
+ class ConflictException(BaseHttpException):
54
+ def __init__(self, detail: str = "conflict"):
55
+ super().__init__(
56
+ status_code=status.HTTP_409_CONFLICT,
57
+ detail=detail,
58
+ )
59
+
60
+
61
+ class NotSupportedException(BaseHttpException):
62
+ def __init__(self, detail: str = "NotSupported"):
63
+ super().__init__(
64
+ status_code=status.HTTP_501_NOT_IMPLEMENTED,
65
+ detail=detail,
66
+ )
67
+
68
+
69
+ class UnprocessableEntityException(BaseHttpException):
70
+ def __init__(self, detail: str, loc: Iterable[Any] | None = None, type_: str = "unprocessable_entity"):
5
71
  super().__init__(
6
72
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
7
- detail={"msg": detail, "loc": loc or []},
73
+ detail=_errors(loc, detail, type_),
8
74
  )
75
+
76
+
77
+ class LogicalValidationException(UnprocessableEntityException):
78
+ def __init__(self, detail: str, loc: Iterable[Any] | None = None, type_: str = "logical_error"):
79
+ super().__init__(detail=detail, loc=loc, type_=type_)
80
+
81
+
@@ -1 +1,20 @@
1
- from .offset_base import PaginateResponse, PaginateRequest
1
+ from typing import Generic, List, Optional, TypeVar
2
+ from pydantic import BaseModel
3
+ from fastapi import Depends
4
+
5
+ from .offset_base import PaginateResponse, PaginateRequest
6
+
7
+
8
+ def get_pagination(
9
+ req: PaginateRequest = Depends(PaginateRequest)
10
+ ) -> PaginateRequest:
11
+ return req
12
+
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ class Paginated(BaseModel, Generic[T]):
18
+ total_count: Optional[int]
19
+ total_page: Optional[int]
20
+ data: List[T]
@@ -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.38
3
+ Version: 0.3.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
@@ -4,20 +4,14 @@ 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
6
  nlbone/adapters/auth/keycloak.py,sha256=lPmRmCwBuDj9fUJsGMUCOuk_MsuLBQYBrO3QvBBaV8I,2451
7
- nlbone/adapters/db/__init__.py,sha256=aHur2GuykZd26RpEmIbkAfflkksZuKWAlYLJMIYotQE,144
7
+ nlbone/adapters/db/__init__.py,sha256=VzhE_VOTFxDE0yUOw-f8UCLFCVhdyKnrrhbqzZUy76A,218
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
11
- nlbone/adapters/db/sqlalchemy/__init__.py,sha256=REK4P8SF6kvDNYgkZIbSowhHPivyFVnMCYGfYT1dlxk,158
10
+ nlbone/adapters/db/sqlalchemy/__init__.py,sha256=1yw3keC1GYqQ6AM4283if_TpeB0xbm0-FBD7XUf5bcg,67
12
11
  nlbone/adapters/db/sqlalchemy/base.py,sha256=-5FnCMKETJ2xykhViHQuNBdbRMaxuieuatgiEl4Lllw,73
13
12
  nlbone/adapters/db/sqlalchemy/engine.py,sha256=m3nVY7KU12ksM3vK9fEgYEobrz9L8hfsgjI8cSNtBMY,3030
13
+ nlbone/adapters/db/sqlalchemy/query_builder.py,sha256=mgj0mE0tbBVOAm1nd2nfR3p4EgV_NPYASsfP_4lF6yA,8504
14
14
  nlbone/adapters/db/sqlalchemy/schema.py,sha256=iiE42UT-DJh-ohezLFBWTBFN5WtrfZdtKToQDNLvoOs,1044
15
- nlbone/adapters/db/sqlalchemy/query/__init__.py,sha256=9SFkoNkklaji6kZt_Nl6X6vqMFE92GiqW36Zezr5PuE,129
16
- nlbone/adapters/db/sqlalchemy/query/builder.py,sha256=AzUX2MlfrMGY-EITzVJoq9naM5sTL9h9yzI7cCeEBRk,886
17
- nlbone/adapters/db/sqlalchemy/query/coercion.py,sha256=uYClfEuew_W4r-vbwj2c5MVpayejhpOMl6h4iUrMotQ,2142
18
- nlbone/adapters/db/sqlalchemy/query/filters.py,sha256=GL6r2l8zY-9Ys6Ge6slI0FTZ8NUrYZzbvd5OwHFyJCg,2421
19
- nlbone/adapters/db/sqlalchemy/query/ordering.py,sha256=THbuxZmoFZzJIa28KfDQ6ioS88MVvwLBb9iIj6QnEGM,490
20
- nlbone/adapters/db/sqlalchemy/query/types.py,sha256=M2j6SOSyFkLuyXq4kvPYgZQYz5TKCBe3osoIIN1yfBI,287
21
15
  nlbone/adapters/http_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
16
  nlbone/adapters/http_clients/email_gateway.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
17
  nlbone/adapters/http_clients/uploadchi.py,sha256=qguGDQ2NGNSN3BOFSFUINn8mKS4LI23UGURJNhDUH2E,4647
@@ -26,7 +20,7 @@ nlbone/adapters/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
26
20
  nlbone/adapters/messaging/redis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
21
  nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
22
  nlbone/config/logging.py,sha256=68WRQejEpL6eHEY_cOgdlOjndKc-RWth0n4YmXnceC8,5041
29
- nlbone/config/settings.py,sha256=i89s5Jbb3qzW8vYXjxrm__bx0OKhFvEfqltwE95lVMc,3278
23
+ nlbone/config/settings.py,sha256=2PEGXJwmhjHK9wuX2AKCfPZWz3F1dvJ5ig4Sad7jqlc,3274
30
24
  nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
25
  nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
26
  nlbone/core/application/services.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -43,17 +37,19 @@ nlbone/core/ports/messaging.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
43
37
  nlbone/core/ports/repo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
38
  nlbone/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
39
  nlbone/interfaces/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
- nlbone/interfaces/api/exceptions.py,sha256=OczR1FND2nbCngwbfgaPrgDbDaz4Pc47mFSHgtL7tW8,313
40
+ nlbone/interfaces/api/exception_handlers.py,sha256=Z3_dTGAmpHfgaMe_HTW89OpRtq-aamzaMTNkMIOdW3w,4092
41
+ nlbone/interfaces/api/exceptions.py,sha256=uJWNEu5-cgoMedYebNHuIFJioXl_fnBhO89E6FINT2A,2259
47
42
  nlbone/interfaces/api/routers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
43
  nlbone/interfaces/api/schemas.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
44
  nlbone/interfaces/api/dependencies/__init__.py,sha256=XrTGkHS8xqryfyr8XSm_s9sIzp4E0b44XQ40sVWcKMY,46
45
+ nlbone/interfaces/api/dependencies/auth.py,sha256=XwzKga9GkcvCKUg37zEnVw8m30MbR-4wh1CTeLG2jH8,1445
50
46
  nlbone/interfaces/api/dependencies/db.py,sha256=IqDVq1lcCCxd22FBUg523lVANM_j71BYAQtsbrHc4M8,465
51
47
  nlbone/interfaces/api/middleware/__init__.py,sha256=Xcxg9Oy8uToPXaTSdBTKhst-hZwsaIEhqxx4mmo1bZI,157
52
48
  nlbone/interfaces/api/middleware/access_log.py,sha256=dEjk_m4fyQ72S2xLzDydDoaw2F9Tvmfl_acat1YThE0,972
53
49
  nlbone/interfaces/api/middleware/add_request_context.py,sha256=i8EV4BvZyrBcNfU-uTkybr7J7ypYvJq8mXSntM6oel4,1816
54
50
  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
51
+ nlbone/interfaces/api/pagination/__init__.py,sha256=zrfmxpaeYoAQ5AjX-AKivs8Zq5Qbr4hJmGzQU4UIN14,426
52
+ nlbone/interfaces/api/pagination/offset_base.py,sha256=2MINXX4wXoynOTPgn28lp_UQufBUluLh_qjF-5AYFxk,3655
57
53
  nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
54
  nlbone/interfaces/cli/init_db.py,sha256=6Q05gwcPa6ywwz3Dzi0hiV8bycg0zU_3eWGsL6VNVRk,479
59
55
  nlbone/interfaces/cli/main.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -62,7 +58,7 @@ nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
62
58
  nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
59
  nlbone/utils/context.py,sha256=AUiN1jM0ebNMopZQoJSqWTfUHuVrp-HV8x6g7QsbEJ8,1601
64
60
  nlbone/utils/time.py,sha256=dC0ucyAmHdNf3wpA_JPinl2VJRubWqx2vcRpJsT3-0k,102
65
- nlbone-0.1.38.dist-info/METADATA,sha256=L3cTFZX7MTAIFXPgLQV9S-EE6OLoccU2vKqsoFoAU4w,2299
66
- nlbone-0.1.38.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
67
- nlbone-0.1.38.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- nlbone-0.1.38.dist-info/RECORD,,
61
+ nlbone-0.3.0.dist-info/METADATA,sha256=XLZxDE3P0_Wowudv8zcNzyiUtOvvPWdt5TiDK0WAGBw,2298
62
+ nlbone-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
63
+ nlbone-0.3.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
+ nlbone-0.3.0.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- from .builder import apply_pagination, apply_filters, apply_order
2
-
3
- __all__ = ["apply_pagination", "apply_filters", "apply_order"]
@@ -1,21 +0,0 @@
1
- from __future__ import annotations
2
- from sqlalchemy.orm import Session, Query
3
-
4
- from nlbone.adapters.db.sqlalchemy.query.filters import apply_filters_to_query
5
- from nlbone.adapters.db.sqlalchemy.query.ordering import apply_order_to_query
6
-
7
-
8
- def apply_pagination(pagination, entity, session: Session, limit: bool = True) -> Query:
9
- q: Query = session.query(entity)
10
- q = apply_filters_to_query(pagination, entity, q)
11
- from nlbone.adapters.db.sqlalchemy.query.ordering import apply_order_to_query
12
- q = apply_order_to_query(pagination, entity, q)
13
- if limit:
14
- q = q.limit(pagination.limit).offset(pagination.offset)
15
- return q
16
-
17
- def apply_filters(pagination, entity, query: Query) -> Query:
18
- return apply_filters_to_query(pagination, entity, query)
19
-
20
- def apply_order(pagination, entity, query: Query) -> Query:
21
- return apply_order_to_query(pagination, entity, query)
@@ -1,64 +0,0 @@
1
- from sqlalchemy.sql.sqltypes import (
2
- String, Text, Integer, BigInteger, SmallInteger, Numeric, Float, Boolean, Enum as SAEnum
3
- )
4
- try:
5
- from sqlalchemy.dialects.postgresql import ENUM as PGEnum
6
- except Exception:
7
- PGEnum = type("PGEnum", (), {})
8
-
9
- from .types import InvalidEnum
10
-
11
- def is_text_type(coltype) -> bool:
12
- return isinstance(coltype, (String, Text))
13
-
14
- def looks_like_wildcard(s: str) -> bool:
15
- return isinstance(s, str) and ("*" in s or "%" in s)
16
-
17
- def to_like_pattern(s: str) -> str:
18
- s = (s or "")
19
- s = s.replace("*", "%")
20
- return s if "%" in s else f"%{s}%"
21
-
22
- def _coerce_enum(col_type, raw):
23
- if raw is None:
24
- return None
25
- enum_cls = getattr(col_type, "enum_class", None)
26
- if enum_cls is not None:
27
- if isinstance(raw, enum_cls):
28
- return raw
29
- if isinstance(raw, str):
30
- low = raw.strip().lower()
31
- for m in enum_cls:
32
- if m.name.lower() == low or str(m.value).lower() == low:
33
- return m
34
- raise InvalidEnum(f"'{raw}' is not one of {[m.name for m in enum_cls]}")
35
- choices = list(getattr(col_type, "enums", []) or [])
36
- if isinstance(raw, str):
37
- low = raw.strip().lower()
38
- for c in choices:
39
- if c.lower() == low:
40
- return c
41
- raise InvalidEnum(f"'{raw}' is not one of {choices or '[no choices defined]'}")
42
-
43
- def coerce_value_for_column(coltype, v):
44
- if v is None:
45
- return None
46
- if isinstance(coltype, (SAEnum, PGEnum)):
47
- return _coerce_enum(coltype, v)
48
- if is_text_type(coltype):
49
- return str(v)
50
- if isinstance(coltype, (Integer, BigInteger, SmallInteger)):
51
- return int(v)
52
- if isinstance(coltype, (Float, Numeric)):
53
- return float(v)
54
- if isinstance(coltype, Boolean):
55
- if isinstance(v, bool):
56
- return v
57
- if isinstance(v, (int, float)):
58
- return bool(v)
59
- if isinstance(v, str):
60
- vl = v.strip().lower()
61
- if vl in {"true", "1", "yes", "y", "t"}: return True
62
- if vl in {"false", "0", "no", "n", "f"}: return False
63
- return None
64
- return v
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
- from sqlalchemy import or_
3
- from sqlalchemy.orm import Query
4
-
5
- try:
6
- from sqlalchemy.dialects.postgresql import ENUM as PGEnum # اختیاری
7
- except Exception:
8
- PGEnum = type("PGEnum", (), {}) # fallback dummy
9
-
10
- from nlbone.interfaces.api.exceptions import UnprocessableEntityException
11
- from nlbone.interfaces.api.pagination import PaginateRequest
12
- from .coercion import coerce_value_for_column, to_like_pattern, is_text_type, looks_like_wildcard
13
- from .types import NULL_SENTINELS, InvalidEnum, parse_field_and_op
14
-
15
- def apply_filters_to_query(pagination: PaginateRequest, entity, query: Query) -> Query:
16
- if not getattr(pagination, "filters", None):
17
- return query
18
-
19
- for raw_field, value in pagination.filters.items():
20
- if value is None or value in NULL_SENTINELS or value == [] or value == {}:
21
- value = None
22
-
23
- field, op_hint = parse_field_and_op(raw_field)
24
- if not hasattr(entity, field):
25
- continue
26
-
27
- col = getattr(entity, field)
28
- coltype = getattr(col, "type", None)
29
-
30
- def _use_ilike(v) -> bool:
31
- if op_hint == "ilike":
32
- return True
33
- return is_text_type(coltype) and isinstance(v, str) and looks_like_wildcard(v)
34
-
35
- try:
36
- if isinstance(value, (list, tuple, set)):
37
- vals = [v for v in value if v not in (None, "", "null", "None")]
38
- if not vals:
39
- continue
40
- if any(_use_ilike(v) for v in vals) and is_text_type(coltype):
41
- patterns = [to_like_pattern(str(v)) for v in vals]
42
- query = query.filter(or_(*[col.ilike(p) for p in patterns]))
43
- else:
44
- coerced = [coerce_value_for_column(coltype, v) for v in vals]
45
- if not coerced:
46
- continue
47
- query = query.filter(col.in_(coerced))
48
- else:
49
- if _use_ilike(value) and is_text_type(coltype):
50
- query = query.filter(col.ilike(to_like_pattern(str(value))))
51
- else:
52
- v = coerce_value_for_column(coltype, value)
53
- query = query.filter(col.is_(None) if v is None else (col == v))
54
- except InvalidEnum as e:
55
- raise UnprocessableEntityException(str(e), loc=["query", "filters", raw_field]) from e
56
-
57
- return query
@@ -1,14 +0,0 @@
1
- from sqlalchemy import asc, desc
2
- from sqlalchemy.orm import Query
3
-
4
- def apply_order_to_query(pagination, entity, query: Query) -> Query:
5
- if not pagination.sort:
6
- return query
7
- clauses = []
8
- for s in pagination.sort:
9
- field = s["field"]
10
- order = s["order"]
11
- if hasattr(entity, field):
12
- col = getattr(entity, field)
13
- clauses.append(asc(col) if order == "asc" else desc(col))
14
- return query.order_by(*clauses) if clauses else query
@@ -1,11 +0,0 @@
1
- NULL_SENTINELS = {"None", "null", ""}
2
-
3
- class InvalidEnum(Exception):
4
- pass
5
-
6
- def parse_field_and_op(field: str) -> tuple[str, str]:
7
- if "__" in field:
8
- base, op = field.rsplit("__", 1)
9
- if op.lower() == "ilike":
10
- return base, "ilike"
11
- return field, "eq"