nlbone 0.6.0__py3-none-any.whl → 0.6.9__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.
Files changed (39) hide show
  1. nlbone/adapters/__init__.py +1 -0
  2. nlbone/adapters/auth/keycloak.py +1 -1
  3. nlbone/adapters/auth/token_provider.py +1 -1
  4. nlbone/adapters/cache/async_redis.py +18 -8
  5. nlbone/adapters/cache/memory.py +21 -11
  6. nlbone/adapters/cache/pubsub_listener.py +3 -0
  7. nlbone/adapters/cache/redis.py +23 -8
  8. nlbone/adapters/db/__init__.py +0 -1
  9. nlbone/adapters/db/postgres/audit.py +14 -11
  10. nlbone/adapters/db/postgres/query_builder.py +103 -86
  11. nlbone/adapters/db/redis/client.py +1 -4
  12. nlbone/adapters/http_clients/__init__.py +2 -2
  13. nlbone/adapters/http_clients/pricing/__init__.py +1 -1
  14. nlbone/adapters/http_clients/pricing/pricing_service.py +40 -20
  15. nlbone/adapters/http_clients/uploadchi/__init__.py +1 -1
  16. nlbone/adapters/http_clients/uploadchi/uploadchi.py +12 -12
  17. nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +14 -15
  18. nlbone/adapters/percolation/__init__.py +1 -1
  19. nlbone/adapters/percolation/connection.py +2 -1
  20. nlbone/config/logging.py +54 -24
  21. nlbone/container.py +14 -9
  22. nlbone/core/application/base_worker.py +1 -1
  23. nlbone/core/domain/models.py +4 -2
  24. nlbone/core/ports/cache.py +25 -9
  25. nlbone/interfaces/api/dependencies/auth.py +26 -0
  26. nlbone/interfaces/api/pagination/offset_base.py +14 -12
  27. nlbone/interfaces/cli/init_db.py +1 -1
  28. nlbone/interfaces/cli/main.py +6 -5
  29. nlbone/utils/cache.py +10 -0
  30. nlbone/utils/cache_keys.py +6 -0
  31. nlbone/utils/cache_registry.py +5 -2
  32. nlbone/utils/http.py +1 -1
  33. nlbone/utils/redactor.py +2 -1
  34. nlbone/utils/time.py +1 -1
  35. {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/METADATA +1 -1
  36. {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/RECORD +39 -39
  37. {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/WHEEL +0 -0
  38. {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/entry_points.txt +0 -0
  39. {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
+ from decimal import Decimal
1
2
  from enum import Enum
2
- from typing import Optional, Literal, List
3
+ from typing import Dict, List, Literal, Optional, Union
3
4
 
4
5
  import httpx
5
6
  import requests
@@ -7,7 +8,7 @@ from pydantic import BaseModel, Field, NonNegativeInt, RootModel
7
8
 
8
9
  from nlbone.adapters.auth.token_provider import ClientTokenProvider
9
10
  from nlbone.config.settings import get_settings
10
- from nlbone.utils.http import normalize_https_base, auth_headers
11
+ from nlbone.utils.http import auth_headers, normalize_https_base
11
12
 
12
13
 
13
14
  class PricingError(Exception):
@@ -32,11 +33,18 @@ class Product(BaseModel):
32
33
 
33
34
 
34
35
  class Pricing(BaseModel):
35
- source: Literal["formula", "static"]
36
- price: float
36
+ source: Optional[Literal["formula", "static"]] = None
37
+ price: Optional[float] = None
37
38
  discount: Optional[float] = None
38
39
  discount_type: Optional[DiscountType] = None
39
- params: dict
40
+ params: Optional[dict] = None
41
+
42
+
43
+ class Segment(BaseModel):
44
+ id: str
45
+ name: str
46
+ specificity: int
47
+ matched_fields: list
40
48
 
41
49
 
42
50
  class Formula(BaseModel):
@@ -49,24 +57,22 @@ class Formula(BaseModel):
49
57
 
50
58
  class PricingRule(BaseModel):
51
59
  product: Product
52
- segment_name: str | None
60
+ segment: Segment | None
53
61
  formula: Optional[Formula] = None
54
- specificity: int
55
- matched_fields: list
56
62
  pricing: Pricing
57
63
 
58
64
 
59
- class CalculatePriceOut(RootModel[List[PricingRule]]):
65
+ class CalculatePriceOut(RootModel[Union[List[PricingRule], Dict[str, PricingRule]]]):
60
66
  pass
61
67
 
62
68
 
63
69
  class PricingService:
64
70
  def __init__(
65
- self,
66
- token_provider: ClientTokenProvider,
67
- base_url: Optional[str] = None,
68
- timeout_seconds: Optional[float] = None,
69
- client: httpx.Client | None = None,
71
+ self,
72
+ token_provider: ClientTokenProvider,
73
+ base_url: Optional[str] = None,
74
+ timeout_seconds: Optional[float] = None,
75
+ client: httpx.Client | None = None,
70
76
  ) -> None:
71
77
  s = get_settings()
72
78
  self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
@@ -74,17 +80,16 @@ class PricingService:
74
80
  self._client = client or requests.session()
75
81
  self._token_provider = token_provider
76
82
 
77
- def calculate(self, items: list[CalculatePriceIn]) -> CalculatePriceOut:
78
- body = {
79
- "items": [i.model_dump() for i in items]
80
- }
83
+ def calculate(self, items: list[CalculatePriceIn], response: Literal["list", "dict"] = "dict") -> CalculatePriceOut:
84
+ body = {"items": [i.model_dump() for i in items]}
81
85
 
82
86
  r = self._client.post(
83
- f"{self._base_url}/priced",
87
+ f"{self._base_url}/price/calculate",
88
+ params={"response": response},
84
89
  headers=auth_headers(self._token_provider.get_access_token()),
85
90
  json=body,
86
91
  timeout=self._timeout,
87
- verify=False
92
+ verify=False,
88
93
  )
89
94
 
90
95
  if r.status_code not in (200, 204):
@@ -94,3 +99,18 @@ class PricingService:
94
99
  return CalculatePriceOut.model_validate(root=[])
95
100
 
96
101
  return CalculatePriceOut.model_validate(r.json())
102
+
103
+ def exchange_rates(self) -> Dict[str, Decimal]:
104
+ r = self._client.get(
105
+ f"{self._base_url}/variables/key/exchange_rates",
106
+ headers=auth_headers(self._token_provider.get_access_token()),
107
+ timeout=self._timeout,
108
+ verify=False,
109
+ )
110
+
111
+ if r.status_code != 200:
112
+ raise PricingError(r.status_code, r.text)
113
+
114
+ values = r.json().get("values")
115
+
116
+ return {f"{value['key']}": Decimal(value["value"]) for value in values}
@@ -1,2 +1,2 @@
1
1
  from .uploadchi import UploadchiClient, UploadchiError
2
- from .uploadchi_async import UploadchiAsyncClient
2
+ from .uploadchi_async import UploadchiAsyncClient
@@ -35,11 +35,11 @@ def _filename_from_cd(cd: str | None, fallback: str) -> str:
35
35
 
36
36
  class UploadchiClient(FileServicePort):
37
37
  def __init__(
38
- self,
39
- token_provider: ClientTokenProvider | None = None,
40
- base_url: Optional[str] = None,
41
- timeout_seconds: Optional[float] = None,
42
- client: httpx.Client | None = None,
38
+ self,
39
+ token_provider: ClientTokenProvider | None = None,
40
+ base_url: Optional[str] = None,
41
+ timeout_seconds: Optional[float] = None,
42
+ client: httpx.Client | None = None,
43
43
  ) -> None:
44
44
  s = get_settings()
45
45
  self._base_url = normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
@@ -51,7 +51,7 @@ class UploadchiClient(FileServicePort):
51
51
  self._client.close()
52
52
 
53
53
  def upload_file(
54
- self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
54
+ self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
55
55
  ) -> dict:
56
56
  tok = _resolve_token(token)
57
57
  files = {"file": (filename, file_bytes)}
@@ -84,12 +84,12 @@ class UploadchiClient(FileServicePort):
84
84
  raise UploadchiError(r.status_code, r.text)
85
85
 
86
86
  def list_files(
87
- self,
88
- limit: int = 10,
89
- offset: int = 0,
90
- filters: dict[str, Any] | None = None,
91
- sort: list[tuple[str, str]] | None = None,
92
- token: str | None = None,
87
+ self,
88
+ limit: int = 10,
89
+ offset: int = 0,
90
+ filters: dict[str, Any] | None = None,
91
+ sort: list[tuple[str, str]] | None = None,
92
+ token: str | None = None,
93
93
  ) -> dict:
94
94
  tok = _resolve_token(token)
95
95
  q = build_list_query(limit, offset, filters, sort)
@@ -4,21 +4,20 @@ from typing import Any, AsyncIterator, Optional
4
4
 
5
5
  import httpx
6
6
 
7
+ from nlbone.adapters.auth.token_provider import ClientTokenProvider
8
+ from nlbone.adapters.http_clients.uploadchi.uploadchi import UploadchiError, _filename_from_cd, _resolve_token
7
9
  from nlbone.config.settings import get_settings
8
10
  from nlbone.core.ports.files import AsyncFileServicePort
9
-
10
- from nlbone.adapters.http_clients.uploadchi.uploadchi import UploadchiError, _filename_from_cd, _resolve_token
11
- from nlbone.adapters.auth.token_provider import ClientTokenProvider
12
11
  from nlbone.utils.http import auth_headers, build_list_query
13
12
 
14
13
 
15
14
  class UploadchiAsyncClient(AsyncFileServicePort):
16
15
  def __init__(
17
- self,
18
- token_provider: ClientTokenProvider | None = None,
19
- base_url: Optional[str] = None,
20
- timeout_seconds: Optional[float] = None,
21
- client: httpx.AsyncClient | None = None,
16
+ self,
17
+ token_provider: ClientTokenProvider | None = None,
18
+ base_url: Optional[str] = None,
19
+ timeout_seconds: Optional[float] = None,
20
+ client: httpx.AsyncClient | None = None,
22
21
  ) -> None:
23
22
  s = get_settings()
24
23
  self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
@@ -32,7 +31,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
32
31
  await self._client.aclose()
33
32
 
34
33
  async def upload_file(
35
- self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
34
+ self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
36
35
  ) -> dict:
37
36
  tok = _resolve_token(token)
38
37
  files = {"file": (filename, file_bytes)}
@@ -63,12 +62,12 @@ class UploadchiAsyncClient(AsyncFileServicePort):
63
62
  raise UploadchiError(r.status_code, await r.aread())
64
63
 
65
64
  async def list_files(
66
- self,
67
- limit: int = 10,
68
- offset: int = 0,
69
- filters: dict[str, Any] | None = None,
70
- sort: list[tuple[str, str]] | None = None,
71
- token: str | None = None,
65
+ self,
66
+ limit: int = 10,
67
+ offset: int = 0,
68
+ filters: dict[str, Any] | None = None,
69
+ sort: list[tuple[str, str]] | None = None,
70
+ token: str | None = None,
72
71
  ) -> dict:
73
72
  tok = _resolve_token(token)
74
73
  q = build_list_query(limit, offset, filters, sort)
@@ -1 +1 @@
1
- from .connection import get_es_client
1
+ from .connection import get_es_client
@@ -4,9 +4,10 @@ from nlbone.config.settings import get_settings
4
4
 
5
5
  setting = get_settings()
6
6
 
7
+
7
8
  def get_es_client():
8
9
  es = Elasticsearch(
9
10
  setting.ELASTIC_PERCOLATE_URL,
10
- basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip())
11
+ basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip()),
11
12
  )
12
13
  return es
nlbone/config/logging.py CHANGED
@@ -11,6 +11,7 @@ from nlbone.utils.redactor import PiiRedactor
11
11
 
12
12
  settings = get_settings()
13
13
 
14
+
14
15
  # ---------- Filters ----------
15
16
  class ContextFilter(logging.Filter):
16
17
  def filter(self, record: logging.LogRecord) -> bool:
@@ -21,12 +22,32 @@ class ContextFilter(logging.Filter):
21
22
  record.user_agent = ctx.get("user_agent")
22
23
  return True
23
24
 
25
+
24
26
  # ---------- Formatter ----------
25
27
  class JsonFormatter(logging.Formatter):
26
28
  RESERVED = {
27
- "args","asctime","created","exc_info","exc_text","filename","funcName","levelname","levelno",
28
- "lineno","module","msecs","message","msg","name","pathname","process","processName",
29
- "relativeCreated","stack_info","thread","threadName",
29
+ "args",
30
+ "asctime",
31
+ "created",
32
+ "exc_info",
33
+ "exc_text",
34
+ "filename",
35
+ "funcName",
36
+ "levelname",
37
+ "levelno",
38
+ "lineno",
39
+ "module",
40
+ "msecs",
41
+ "message",
42
+ "msg",
43
+ "name",
44
+ "pathname",
45
+ "process",
46
+ "processName",
47
+ "relativeCreated",
48
+ "stack_info",
49
+ "thread",
50
+ "threadName",
30
51
  }
31
52
 
32
53
  def format(self, record: logging.LogRecord) -> str:
@@ -53,17 +74,23 @@ class JsonFormatter(logging.Formatter):
53
74
 
54
75
  return json.dumps(payload, ensure_ascii=False)
55
76
 
77
+
56
78
  class PlainFormatter(logging.Formatter):
57
79
  def __init__(self):
58
80
  super().__init__(
59
- fmt="%(asctime)s | %(levelname)s | %(name)s | "
60
- "req=%(request_id)s user=%(user_id)s ip=%(ip)s | %(message)s",
81
+ fmt="%(asctime)s | %(levelname)s | %(name)s | req=%(request_id)s user=%(user_id)s ip=%(ip)s | %(message)s",
61
82
  datefmt="%Y-%m-%dT%H:%M:%S%z",
62
83
  )
63
84
 
85
+
64
86
  # ---------- Setup ----------
65
- def setup_logging(*, log_json: bool=settings.LOG_JSON, log_level: str =settings.LOG_LEVEL,
66
- log_file: str | None = None, silence_uvicorn_access: bool = True):
87
+ def setup_logging(
88
+ *,
89
+ log_json: bool = settings.LOG_JSON,
90
+ log_level: str = settings.LOG_LEVEL,
91
+ log_file: str | None = None,
92
+ silence_uvicorn_access: bool = True,
93
+ ):
67
94
  handlers = {
68
95
  "console": {
69
96
  "class": "logging.StreamHandler",
@@ -82,23 +109,25 @@ def setup_logging(*, log_json: bool=settings.LOG_JSON, log_level: str =settings.
82
109
  "formatter": "json" if log_json else "plain",
83
110
  }
84
111
 
85
- dictConfig({
86
- "version": 1,
87
- "disable_existing_loggers": False,
88
- "filters": {
89
- "ctx": {"()": ContextFilter},
90
- "pii": {"()": PiiRedactor},
91
- },
92
- "formatters": {
93
- "json": {"()": JsonFormatter},
94
- "plain": {"()": PlainFormatter},
95
- },
96
- "handlers": handlers,
97
- "root": {
98
- "level": log_level,
99
- "handlers": list(handlers.keys()),
100
- },
101
- })
112
+ dictConfig(
113
+ {
114
+ "version": 1,
115
+ "disable_existing_loggers": False,
116
+ "filters": {
117
+ "ctx": {"()": ContextFilter},
118
+ "pii": {"()": PiiRedactor},
119
+ },
120
+ "formatters": {
121
+ "json": {"()": JsonFormatter},
122
+ "plain": {"()": PlainFormatter},
123
+ },
124
+ "handlers": handlers,
125
+ "root": {
126
+ "level": log_level,
127
+ "handlers": list(handlers.keys()),
128
+ },
129
+ }
130
+ )
102
131
 
103
132
  logging.getLogger("asyncio").setLevel(logging.WARNING)
104
133
  logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -115,5 +144,6 @@ def setup_logging(*, log_json: bool=settings.LOG_JSON, log_level: str =settings.
115
144
  uvicorn_access.handlers = []
116
145
  uvicorn_access.propagate = True
117
146
 
147
+
118
148
  def get_logger(name: str | None = None) -> logging.Logger:
119
149
  return logging.getLogger(name or "app")
nlbone/container.py CHANGED
@@ -15,8 +15,9 @@ from nlbone.adapters.http_clients import PricingService
15
15
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
16
16
  from nlbone.adapters.http_clients.uploadchi.uploadchi_async import UploadchiAsyncClient
17
17
  from nlbone.adapters.messaging import InMemoryEventBus
18
+ from nlbone.config.settings import Settings
18
19
  from nlbone.core.ports import EventBusPort
19
- from nlbone.core.ports.cache import CachePort, AsyncCachePort
20
+ from nlbone.core.ports.cache import AsyncCachePort, CachePort
20
21
  from nlbone.core.ports.files import AsyncFileServicePort, FileServicePort
21
22
 
22
23
 
@@ -36,11 +37,15 @@ class Container(containers.DeclarativeContainer):
36
37
  # --- Services ---
37
38
  auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService, settings=config)
38
39
  token_provider = providers.Singleton(ClientTokenProvider, auth=auth, skew_seconds=30)
39
- file_service: providers.Singleton[FileServicePort] = providers.Singleton(UploadchiClient,
40
- token_provider=token_provider)
41
- afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(UploadchiAsyncClient,
42
- token_provider=token_provider)
43
- pricing_service: providers.Singleton[PricingService] = providers.Singleton(PricingService, token_provider=token_provider)
40
+ file_service: providers.Singleton[FileServicePort] = providers.Singleton(
41
+ UploadchiClient, token_provider=token_provider
42
+ )
43
+ afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(
44
+ UploadchiAsyncClient, token_provider=token_provider
45
+ )
46
+ pricing_service: providers.Singleton[PricingService] = providers.Singleton(
47
+ PricingService, token_provider=token_provider
48
+ )
44
49
 
45
50
  cache: providers.Singleton[CachePort] = providers.Selector(
46
51
  config.CACHE_BACKEND,
@@ -58,12 +63,12 @@ class Container(containers.DeclarativeContainer):
58
63
  def create_container(settings: Optional[Any] = None) -> Container:
59
64
  c = Container()
60
65
  if settings is not None:
61
- if hasattr(settings, "model_dump"):
66
+ if isinstance(settings, Settings):
67
+ c.config.override(settings)
68
+ elif hasattr(settings, "model_dump"):
62
69
  c.config.from_dict(settings.model_dump()) # Pydantic v2
63
70
  elif hasattr(settings, "dict"):
64
71
  c.config.from_dict(settings.dict()) # Pydantic v1
65
72
  elif isinstance(settings, Mapping):
66
73
  c.config.from_dict(dict(settings))
67
- else:
68
- c.config.override(settings)
69
74
  return c
@@ -33,4 +33,4 @@ class BaseWorker(ABC):
33
33
 
34
34
  @abstractmethod
35
35
  async def process(self, *args, **kwargs) -> Any:
36
- pass
36
+ pass
@@ -1,7 +1,8 @@
1
1
  import uuid
2
2
  from datetime import datetime
3
- from sqlalchemy import String, DateTime, Index, Text
3
+
4
4
  from sqlalchemy import JSON as SA_JSON
5
+ from sqlalchemy import DateTime, Index, String, Text
5
6
  from sqlalchemy.orm import Mapped, mapped_column
6
7
  from sqlalchemy.sql import func
7
8
 
@@ -9,6 +10,7 @@ from nlbone.adapters.db import Base
9
10
 
10
11
  try:
11
12
  from sqlalchemy.dialects.postgresql import JSONB, UUID
13
+
12
14
  JSONType = JSONB
13
15
  UUIDType = UUID(as_uuid=True)
14
16
  except Exception:
@@ -35,4 +37,4 @@ class AuditLog(Base):
35
37
  __table_args__ = (
36
38
  Index("ix_audit_entity_entityid", "entity", "entity_id"),
37
39
  Index("ix_audit_created_at", "created_at"),
38
- )
40
+ )
@@ -1,37 +1,53 @@
1
- from typing import Protocol, Optional, Iterable, Any, Mapping, Sequence, Tuple, TypeVar, Callable
1
+ from typing import Any, Callable, Iterable, Mapping, Optional, Protocol, Sequence, TypeVar
2
2
 
3
3
  T = TypeVar("T")
4
4
 
5
+
5
6
  class CachePort(Protocol):
6
7
  def get(self, key: str) -> Optional[bytes]: ...
7
- def set(self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None: ...
8
+ def set(
9
+ self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
10
+ ) -> None: ...
8
11
  def delete(self, key: str) -> None: ...
9
12
  def exists(self, key: str) -> bool: ...
10
13
  def ttl(self, key: str) -> Optional[int]: ...
11
14
 
12
15
  def mget(self, keys: Sequence[str]) -> list[Optional[bytes]]: ...
13
- def mset(self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None: ...
16
+ def mset(
17
+ self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
18
+ ) -> None: ...
14
19
 
15
20
  def get_json(self, key: str) -> Optional[Any]: ...
16
- def set_json(self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None: ...
21
+ def set_json(
22
+ self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
23
+ ) -> None: ...
17
24
 
18
25
  def invalidate_tags(self, tags: Iterable[str]) -> int: ...
19
26
  def bump_namespace(self, namespace: str) -> int: ... # versioned keys
20
27
  def clear_namespace(self, namespace: str) -> int: ...
21
28
 
22
- def get_or_set(self, key: str, producer: Callable[[], bytes], *, ttl: int, tags: Optional[Iterable[str]] = None) -> bytes: ...
29
+ def get_or_set(
30
+ self, key: str, producer: Callable[[], bytes], *, ttl: int, tags: Optional[Iterable[str]] = None
31
+ ) -> bytes: ...
32
+
23
33
 
24
34
  class AsyncCachePort(Protocol):
25
35
  async def get(self, key: str) -> Optional[bytes]: ...
26
- async def set(self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None: ...
36
+ async def set(
37
+ self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
38
+ ) -> None: ...
27
39
  async def delete(self, key: str) -> None: ...
28
40
  async def exists(self, key: str) -> bool: ...
29
41
  async def ttl(self, key: str) -> Optional[int]: ...
30
42
  async def mget(self, keys: Sequence[str]) -> list[Optional[bytes]]: ...
31
- async def mset(self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None: ...
43
+ async def mset(
44
+ self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
45
+ ) -> None: ...
32
46
  async def get_json(self, key: str) -> Optional[Any]: ...
33
- async def set_json(self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None: ...
47
+ async def set_json(
48
+ self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
49
+ ) -> None: ...
34
50
  async def invalidate_tags(self, tags: Iterable[str]) -> int: ...
35
51
  async def bump_namespace(self, namespace: str) -> int: ...
36
52
  async def clear_namespace(self, namespace: str) -> int: ...
37
- async def get_or_set(self, key: str, producer, *, ttl: int, tags: Optional[Iterable[str]] = None) -> bytes: ...
53
+ async def get_or_set(self, key: str, producer, *, ttl: int, tags: Optional[Iterable[str]] = None) -> bytes: ...
@@ -59,3 +59,29 @@ def has_access(*, permissions=None):
59
59
  return wrapper
60
60
 
61
61
  return decorator
62
+
63
+
64
+ def client_or_user_has_access(*, permissions=None, client_permissions=None):
65
+ def decorator(func):
66
+ @functools.wraps(func)
67
+ def wrapper(*args, **kwargs):
68
+ request = current_request()
69
+ token = getattr(request.state, "token", None)
70
+ if not token:
71
+ raise UnauthorizedException()
72
+
73
+ auth = KeycloakAuthService()
74
+
75
+ if auth.get_client_id(token):
76
+ needed = client_permissions or permissions
77
+ if not auth.client_has_access(token, permissions=needed):
78
+ raise ForbiddenException(f"Forbidden (client) {needed}")
79
+ else:
80
+ if not current_user_id():
81
+ raise UnauthorizedException()
82
+ if not auth.has_access(token, permissions=permissions):
83
+ raise ForbiddenException(f"Forbidden (user) {permissions}")
84
+
85
+ return func(*args, **kwargs)
86
+ return wrapper
87
+ return decorator
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from math import ceil
3
- from typing import Any, Optional
3
+ from typing import Any, Optional, List
4
4
 
5
5
  from fastapi import Query
6
6
 
@@ -13,16 +13,18 @@ class PaginateRequest:
13
13
  """
14
14
 
15
15
  def __init__(
16
- self,
17
- limit: int = 10,
18
- offset: int = 0,
19
- sort: Optional[str] = None,
20
- filters: Optional[str] = Query(None, description="e.g. title:abc"),
16
+ self,
17
+ limit: int = 10,
18
+ offset: int = 0,
19
+ sort: Optional[str] = None,
20
+ filters: Optional[str] = Query(None, description="e.g. title:abc"),
21
+ include: Optional[str] = None,
21
22
  ) -> None:
22
23
  self.limit = max(0, limit)
23
24
  self.offset = max(0, offset)
24
25
  self.sort = self._parse_sort(sort)
25
26
  self.filters = self._parse_filters(filters or "")
27
+ self.include_ids: List[int] = ([int(x) for x in include.split(",") if x.strip().isdigit()] if include else [])[:50]
26
28
 
27
29
  @staticmethod
28
30
  def _parse_sort(sort_str: Optional[str]) -> list[dict[str, str]]:
@@ -81,12 +83,12 @@ class PaginateResponse:
81
83
  """
82
84
 
83
85
  def __init__(
84
- self,
85
- data: list[Any],
86
- total_count: int | None,
87
- limit: int,
88
- offset: int,
89
- use_data_key: bool = True,
86
+ self,
87
+ data: list[Any],
88
+ total_count: int | None,
89
+ limit: int,
90
+ offset: int,
91
+ use_data_key: bool = True,
90
92
  ) -> None:
91
93
  self.data = data
92
94
  self.total_count = total_count
@@ -1,6 +1,6 @@
1
1
  import typer
2
2
 
3
- from nlbone.adapters.db import init_sync_engine, Base, sync_ping
3
+ from nlbone.adapters.db import Base, init_sync_engine, sync_ping
4
4
 
5
5
  init_db_command = typer.Typer(help="Database utilities")
6
6
 
@@ -1,6 +1,7 @@
1
- import typer
2
1
  from typing import Optional
3
2
 
3
+ import typer
4
+
4
5
  from nlbone.adapters.db import init_sync_engine
5
6
  from nlbone.config.settings import get_settings
6
7
  from nlbone.interfaces.cli.init_db import init_db_command
@@ -9,12 +10,10 @@ app = typer.Typer(help="NLBone CLI")
9
10
 
10
11
  app.add_typer(init_db_command, name="db")
11
12
 
13
+
12
14
  @app.callback()
13
15
  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
- ),
16
+ env_file: Optional[str] = typer.Option(None, "--env-file", "-e", help="Path to .env file. In prod omit this."),
18
17
  debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
19
18
  ):
20
19
  settings = get_settings(env_file=env_file)
@@ -22,8 +21,10 @@ def common(
22
21
  pass
23
22
  init_sync_engine(echo=settings.DEBUG)
24
23
 
24
+
25
25
  def main():
26
26
  app()
27
27
 
28
+
28
29
  if __name__ == "__main__":
29
30
  main()