nlbone 0.6.19__tar.gz → 0.6.20__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.
Files changed (111) hide show
  1. {nlbone-0.6.19 → nlbone-0.6.20}/PKG-INFO +2 -1
  2. {nlbone-0.6.19 → nlbone-0.6.20}/pyproject.toml +2 -1
  3. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/query_builder.py +1 -1
  4. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/repositories/outbox_repo.py +6 -8
  5. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/config/settings.py +5 -0
  6. nlbone-0.6.20/src/nlbone/core/application/bus.py +169 -0
  7. nlbone-0.6.20/src/nlbone/core/application/di.py +99 -0
  8. nlbone-0.6.20/src/nlbone/core/application/registry.py +123 -0
  9. nlbone-0.6.20/src/nlbone/core/ports/repository.py +115 -0
  10. nlbone-0.6.20/src/nlbone/interfaces/cli/crypto.py +22 -0
  11. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/cli/main.py +2 -0
  12. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/jobs/dispatch_outbox.py +1 -0
  13. nlbone-0.6.20/src/nlbone/utils/crypto.py +29 -0
  14. nlbone-0.6.20/src/nlbone/utils/normalize_mobile.py +33 -0
  15. nlbone-0.6.19/src/nlbone/core/application/command_bus.py +0 -25
  16. {nlbone-0.6.19 → nlbone-0.6.20}/.gitignore +0 -0
  17. {nlbone-0.6.19 → nlbone-0.6.20}/LICENSE +0 -0
  18. {nlbone-0.6.19 → nlbone-0.6.20}/README.md +0 -0
  19. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/__init__.py +0 -0
  20. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/__init__.py +0 -0
  21. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/auth/__init__.py +0 -0
  22. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/auth/keycloak.py +0 -0
  23. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/auth/token_provider.py +0 -0
  24. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/cache/__init__.py +0 -0
  25. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/cache/async_redis.py +0 -0
  26. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/cache/memory.py +0 -0
  27. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/cache/pubsub_listener.py +0 -0
  28. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/cache/redis.py +0 -0
  29. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/__init__.py +0 -0
  30. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
  31. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/audit.py +0 -0
  32. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/base.py +0 -0
  33. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/engine.py +0 -0
  34. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/repository.py +0 -0
  35. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/schema.py +0 -0
  36. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/postgres/uow.py +0 -0
  37. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/redis/__init__.py +0 -0
  38. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/db/redis/client.py +0 -0
  39. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/http_clients/__init__.py +0 -0
  40. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/http_clients/pricing/__init__.py +0 -0
  41. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/http_clients/pricing/pricing_service.py +0 -0
  42. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/http_clients/uploadchi/__init__.py +0 -0
  43. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/http_clients/uploadchi/uploadchi.py +0 -0
  44. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +0 -0
  45. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/messaging/__init__.py +0 -0
  46. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/messaging/event_bus.py +0 -0
  47. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/messaging/redis.py +0 -0
  48. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/percolation/__init__.py +0 -0
  49. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/percolation/connection.py +0 -0
  50. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/adapters/repositories/__init__.py +0 -0
  51. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/config/__init__.py +0 -0
  52. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/config/logging.py +0 -0
  53. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/container.py +0 -0
  54. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/__init__.py +0 -0
  55. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/application/__init__.py +0 -0
  56. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/application/base_worker.py +0 -0
  57. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/application/events.py +0 -0
  58. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/application/services/__init__.py +0 -0
  59. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/application/services.py +0 -0
  60. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/application/use_case.py +0 -0
  61. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/domain/__init__.py +0 -0
  62. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/domain/base.py +0 -0
  63. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/domain/events.py +0 -0
  64. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/domain/models.py +0 -0
  65. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/__init__.py +0 -0
  66. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/auth.py +0 -0
  67. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/cache.py +0 -0
  68. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/event_bus.py +0 -0
  69. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/files.py +0 -0
  70. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/messaging.py +0 -0
  71. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/repo.py +0 -0
  72. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/core/ports/uow.py +0 -0
  73. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/__init__.py +0 -0
  74. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/__init__.py +0 -0
  75. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/additional_filed/__init__.py +0 -0
  76. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/additional_filed/assembler.py +0 -0
  77. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py +0 -0
  78. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py +0 -0
  79. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/additional_filed/field_registry.py +0 -0
  80. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/additional_filed/resolver.py +0 -0
  81. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
  82. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -0
  83. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/dependencies/auth.py +0 -0
  84. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
  85. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
  86. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
  87. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/exceptions.py +0 -0
  88. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
  89. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
  90. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
  91. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
  92. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
  93. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
  94. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/routers.py +0 -0
  95. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/schema/__init__.py +0 -0
  96. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/schema/adaptive_schema.py +0 -0
  97. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/schema/base_response_model.py +0 -0
  98. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/api/schemas.py +0 -0
  99. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/cli/__init__.py +0 -0
  100. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/cli/init_db.py +0 -0
  101. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/jobs/__init__.py +0 -0
  102. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  103. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/types.py +0 -0
  104. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/__init__.py +0 -0
  105. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/cache.py +0 -0
  106. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/cache_keys.py +0 -0
  107. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/cache_registry.py +0 -0
  108. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/context.py +0 -0
  109. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/http.py +0 -0
  110. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/redactor.py +0 -0
  111. {nlbone-0.6.19 → nlbone-0.6.20}/src/nlbone/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.6.19
3
+ Version: 0.6.20
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,6 +8,7 @@ License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: anyio>=4.0
10
10
  Requires-Dist: cachetools>=6.2.0
11
+ Requires-Dist: cryptography~=45.0.4
11
12
  Requires-Dist: dependency-injector>=4.48.1
12
13
  Requires-Dist: elasticsearch==8.14.0
13
14
  Requires-Dist: fastapi>=0.116
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlbone"
7
- version = "0.6.19"
7
+ version = "0.6.20"
8
8
  description = "Backbone package for interfaces and infrastructure in Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -29,6 +29,7 @@ dependencies = [
29
29
  "typer>=0.17.4",
30
30
  "makefun>=1.16.0",
31
31
  "cachetools>=6.2.0",
32
+ "cryptography~=45.0.4",
32
33
  ]
33
34
 
34
35
  [tool.ruff]
@@ -340,7 +340,7 @@ def get_paginated_response(
340
340
  with_count: bool = True,
341
341
  output_cls: Optional[Type] = None,
342
342
  eager_options: Optional[Sequence[LoaderOption]] = None,
343
- query = None
343
+ query=None,
344
344
  ) -> dict:
345
345
  if not query:
346
346
  query = session.query(entity)
@@ -1,20 +1,18 @@
1
1
  from typing import List
2
2
 
3
+
3
4
  class OutboxRecord(dict):
4
5
  pass
5
6
 
7
+
6
8
  class OutboxRepository:
7
9
  def __init__(self, engine):
8
10
  self._engine = engine
9
11
 
10
- def add(self, msg) -> None:
11
- ...
12
+ def add(self, msg) -> None: ...
12
13
 
13
- def fetch_pending(self, limit: int = 100) -> List[OutboxRecord]:
14
- ...
14
+ def fetch_pending(self, limit: int = 100) -> List[OutboxRecord]: ...
15
15
 
16
- def mark_sent(self, msg_id: int) -> None:
17
- ...
16
+ def mark_sent(self, msg_id: int) -> None: ...
18
17
 
19
- def schedule_retry(self, msg_id: int, retries: int, backoff_base: int = 2) -> None:
20
- ...
18
+ def schedule_retry(self, msg_id: int, retries: int, backoff_base: int = 2) -> None: ...
@@ -96,6 +96,11 @@ class Settings(BaseSettings):
96
96
  # ---------------------------
97
97
  PRICING_SERVICE_URL: AnyHttpUrl = Field(default="https://pricing.numberland.ir/v1")
98
98
 
99
+ # ---------------------------
100
+ # Crypto
101
+ # ---------------------------
102
+ FERNET_KEY: str = Field(default="")
103
+
99
104
  model_config = SettingsConfigDict(
100
105
  env_prefix="",
101
106
  env_file=None,
@@ -0,0 +1,169 @@
1
+ import traceback
2
+ from typing import Optional, List, Callable, Coroutine, Any, Protocol
3
+
4
+ from nlbone.core.application.registry import HandlerRegistry, AsyncHandlerRegistry
5
+ from nlbone.core.domain.base import Command, DomainEvent
6
+ from nlbone.core.ports import UnitOfWork, AsyncUnitOfWork
7
+ from nlbone.core.ports.event_bus import OutboxPublisher, Message, AsyncOutboxPublisher
8
+ from nlbone.interfaces.api.middleware.access_log import logger
9
+
10
+
11
+ class SyncMiddleware(Protocol):
12
+ def __call__(self, message: Message, next_: Callable[[Message], None]) -> None: # noqa: D401
13
+ ...
14
+
15
+
16
+ class AsyncMiddleware(Protocol):
17
+ async def __call__(self, message: Message, next_: Callable[[Message], Coroutine[Any, Any, None]]) -> None: ...
18
+
19
+
20
+ class SyncMessageBus:
21
+ def __init__(
22
+ self,
23
+ uow: UnitOfWork,
24
+ registry: HandlerRegistry,
25
+ middlewares: Optional[List[SyncMiddleware]] = None,
26
+ outbox: Optional[OutboxPublisher] = None,
27
+ ) -> None:
28
+ self.uow = uow
29
+ self.registry = registry
30
+ self.middlewares = middlewares or []
31
+ self.outbox = outbox
32
+ self._queue: List[Message] = []
33
+
34
+ def handle(self, message: Message) -> None:
35
+ self._queue.append(message)
36
+ while self._queue:
37
+ msg = self._queue.pop(0)
38
+ self._dispatch_with_pipeline(msg)
39
+
40
+ # pipeline that wraps dispatch
41
+ def _dispatch_with_pipeline(self, message: Message) -> None:
42
+ def terminal(m: Message) -> None:
43
+ self._dispatch(m)
44
+
45
+ # build chain right-to-left
46
+ next_callable = terminal
47
+ for mw in reversed(self.middlewares):
48
+ current_mw = mw
49
+
50
+ def make_next(nxt: Callable[[Message], None]): # closure helper
51
+ def _mw_call(m: Message) -> None:
52
+ return current_mw(m, nxt)
53
+
54
+ return _mw_call
55
+
56
+ next_callable = make_next(next_callable)
57
+ next_callable(message)
58
+
59
+ def _dispatch(self, message: Message) -> None:
60
+ if isinstance(message, DomainEvent):
61
+ self._handle_event(message)
62
+ elif isinstance(message, Command):
63
+ self._handle_command(message)
64
+ else:
65
+ raise TypeError(f"Unknown message type: {type(message)!r}")
66
+
67
+ def _handle_event(self, event: DomainEvent) -> None:
68
+ handlers = self.registry.for_event(type(event))
69
+ for handler in handlers:
70
+ try:
71
+ logger.debug("handling event %s with %s", event, handler)
72
+ produced = handler(event)
73
+ if produced:
74
+ self._queue.extend(produced)
75
+ self._queue.extend(self.uow.collect_new_events())
76
+ except Exception: # noqa: BLE001
77
+ logger.exception("Exception handling event %s\n%s", event, traceback.format_exc())
78
+ continue
79
+
80
+ def _handle_command(self, command: Command) -> None:
81
+ handler = self.registry.for_command(type(command))
82
+ try:
83
+ logger.debug("handling command %s with %s", command, handler)
84
+ produced = handler(command)
85
+ if produced:
86
+ self._queue.extend(produced)
87
+ # commit (and gather/emit domain events)
88
+ self._queue.extend(self.uow.collect_new_events())
89
+ if self.outbox:
90
+ self.outbox.publish(self._queue) # best-effort; in real systems use a DB-backed outbox
91
+ except Exception: # noqa: BLE001
92
+ logger.exception("Exception handling command %s\n%s", command, traceback.format_exc())
93
+ raise
94
+
95
+
96
+ # ==========================
97
+ # MessageBus (async)
98
+ # ==========================
99
+ class AsyncMessageBus:
100
+ def __init__(
101
+ self,
102
+ uow: AsyncUnitOfWork,
103
+ registry: AsyncHandlerRegistry,
104
+ middlewares: Optional[List[AsyncMiddleware]] = None,
105
+ outbox: Optional[AsyncOutboxPublisher] = None,
106
+ ) -> None:
107
+ self.uow = uow
108
+ self.registry = registry
109
+ self.middlewares = middlewares or []
110
+ self.outbox = outbox
111
+ self._queue: List[Message] = []
112
+
113
+ async def handle(self, message: Message) -> None:
114
+ self._queue.append(message)
115
+ while self._queue:
116
+ msg = self._queue.pop(0)
117
+ await self._dispatch_with_pipeline(msg)
118
+
119
+ async def _dispatch_with_pipeline(self, message: Message) -> None:
120
+ async def terminal(m: Message) -> None:
121
+ await self._dispatch(m)
122
+
123
+ next_callable = terminal
124
+ for mw in reversed(self.middlewares):
125
+ current_mw = mw
126
+
127
+ def make_next(nxt: Callable[[Message], Coroutine[Any, Any, None]]):
128
+ async def _mw_call(m: Message) -> None:
129
+ return await current_mw(m, nxt)
130
+
131
+ return _mw_call
132
+
133
+ next_callable = make_next(next_callable)
134
+ await next_callable(message)
135
+
136
+ async def _dispatch(self, message: Message) -> None:
137
+ if isinstance(message, DomainEvent):
138
+ await self._handle_event(message)
139
+ elif isinstance(message, Command):
140
+ await self._handle_command(message)
141
+ else:
142
+ raise TypeError(f"Unknown message type: {type(message)!r}")
143
+
144
+ async def _handle_event(self, event: DomainEvent) -> None:
145
+ handlers = self.registry.for_event(type(event))
146
+ for handler in handlers:
147
+ try:
148
+ logger.debug("handling event %s with %s", event, handler)
149
+ produced = await handler(event)
150
+ if produced:
151
+ self._queue.extend(produced)
152
+ self._queue.extend(self.uow.collect_new_events())
153
+ except Exception: # noqa: BLE001
154
+ logger.exception("Exception handling event %s\n%s", event, traceback.format_exc())
155
+ continue
156
+
157
+ async def _handle_command(self, command: Command) -> None:
158
+ handler = self.registry.for_command(type(command))
159
+ try:
160
+ logger.debug("handling command %s with %s", command, handler)
161
+ produced = await handler(command)
162
+ if produced:
163
+ self._queue.extend(produced)
164
+ self._queue.extend(self.uow.collect_new_events())
165
+ if self.outbox:
166
+ await self.outbox.publish(self._queue)
167
+ except Exception: # noqa: BLE001
168
+ logger.exception("Exception handling command %s\n%s", command, traceback.format_exc())
169
+ raise
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+ import inspect
3
+ from typing import Any, Callable, Dict, Optional, Type, get_type_hints, get_origin, get_args
4
+
5
+ class TypeContainer:
6
+ """Tiny type-based DI: register_instance(T, obj) / register_factory(T, () -> obj)."""
7
+ def __init__(self) -> None:
8
+ self._instances: Dict[Type[Any], Any] = {}
9
+ self._factories: Dict[Type[Any], Callable[[], Any]] = {}
10
+
11
+ def register_instance(self, t: Type[Any], instance: Any) -> None:
12
+ self._instances[t] = instance
13
+
14
+ def register_factory(self, t: Type[Any], factory: Callable[[], Any]) -> None:
15
+ self._factories[t] = factory
16
+
17
+ def _providers(self):
18
+ for t, v in self._instances.items():
19
+ yield t, (lambda v=v: v)
20
+ for t, f in self._factories.items():
21
+ yield t, f
22
+
23
+ def _unwrap(self, ann: Any) -> tuple[list[Type[Any]], bool]:
24
+ if ann is inspect._empty:
25
+ return [], True
26
+ origin = get_origin(ann)
27
+ args = list(get_args(ann))
28
+ allow_none = False
29
+ if origin in (Optional, getattr(__import__("typing"), "Union")):
30
+ if type(None) in args:
31
+ allow_none = True
32
+ args = [a for a in args if a is not type(None)]
33
+ return [a for a in args if isinstance(a, type)], allow_none
34
+ if isinstance(ann, type):
35
+ return [ann], False
36
+ return [], True
37
+
38
+ def resolve(self, ann: Any) -> Any:
39
+ types, allow_none = self._unwrap(ann)
40
+ if not types:
41
+ if allow_none:
42
+ return None
43
+ raise LookupError(f"Cannot resolve {ann!r}")
44
+ for T in types:
45
+ # exact
46
+ for pt, make in self._providers():
47
+ if pt is T:
48
+ return make()
49
+ # supertype/provider match
50
+ best = None
51
+ for pt, make in self._providers():
52
+ try:
53
+ if issubclass(T, pt):
54
+ dist = _mro_distance(T, pt)
55
+ if best is None or dist < best[0]:
56
+ best = (dist, make)
57
+ except TypeError:
58
+ pass
59
+ if best:
60
+ return best[1]()
61
+ if allow_none:
62
+ return None
63
+ raise LookupError(f"No provider for {types}")
64
+
65
+ def _mro_distance(sub: Type[Any], sup: Type[Any]) -> int:
66
+ try:
67
+ return sub.mro().index(sup)
68
+ except ValueError:
69
+ return 10**6
70
+
71
+
72
+ def bind_callable(fn: Callable[..., Any], c: TypeContainer) -> Callable[..., Any]:
73
+ sig = inspect.signature(fn)
74
+ hints = get_type_hints(fn)
75
+ def wrapper(message: Any):
76
+ kwargs = {}
77
+ for name, param in list(sig.parameters.items())[1:]:
78
+ ann = hints.get(name, param.annotation)
79
+ kwargs[name] = c.resolve(ann)
80
+ return fn(message, **kwargs)
81
+ return wrapper
82
+
83
+
84
+ def bind_handler(handler: Any, c: TypeContainer) -> Callable[..., Any]:
85
+ if inspect.isclass(handler):
86
+ init = handler.__init__ # type: ignore[attr-defined]
87
+ sig = inspect.signature(init)
88
+ hints = get_type_hints(init)
89
+ kwargs = {}
90
+ for name, param in list(sig.parameters.items())[1:]:
91
+ ann = hints.get(name, param.annotation)
92
+ kwargs[name] = c.resolve(ann)
93
+ instance = handler(**kwargs) # type: ignore
94
+ if not callable(instance):
95
+ raise TypeError(f"{handler!r} must implement __call__")
96
+ return instance
97
+ elif callable(handler):
98
+ return bind_callable(handler, c)
99
+ raise TypeError(f"Unsupported handler: {handler!r}")
@@ -0,0 +1,123 @@
1
+ from typing import Dict, Type, List, Callable, TypeVar, Optional, Iterable, Coroutine, Any
2
+
3
+ from nlbone.core.application.di import TypeContainer, bind_handler
4
+ from nlbone.core.domain.base import DomainEvent, Command
5
+ from nlbone.core.ports.event_bus import Message
6
+
7
+ TMsg = TypeVar("TMsg", bound=Message)
8
+ SyncHandler = Callable[[TMsg], Optional[Iterable[Message]]]
9
+ AsyncHandler = Callable[[TMsg], Coroutine[Any, Any, Optional[Iterable[Message]]]]
10
+
11
+
12
+ class HandlerRegistry:
13
+ def __init__(self) -> None:
14
+ self._event_handlers: Dict[Type[DomainEvent], List[SyncHandler[Any]]] = {}
15
+ self._command_handlers: Dict[Type[Command], SyncHandler[Any]] = {}
16
+
17
+ def register_event(self, event_type: Type[DomainEvent], handler: SyncHandler[Any]) -> None:
18
+ self._event_handlers.setdefault(event_type, []).append(handler)
19
+
20
+ def register_command(self, cmd_type: Type[Command], handler: SyncHandler[Any]) -> None:
21
+ if cmd_type in self._command_handlers:
22
+ raise ValueError(f"Command handler already registered for {cmd_type!r}")
23
+ self._command_handlers[cmd_type] = handler
24
+
25
+ def for_event(self, event_type: Type[DomainEvent]) -> List[SyncHandler[Any]]:
26
+ return self._event_handlers.get(event_type, [])
27
+
28
+ def for_command(self, cmd_type: Type[Command]) -> SyncHandler[Any]:
29
+ try:
30
+ return self._command_handlers[cmd_type]
31
+ except KeyError as e:
32
+ raise KeyError(f"No handler for command {cmd_type.__name__}") from e
33
+
34
+
35
+ class AsyncHandlerRegistry:
36
+ def __init__(self) -> None:
37
+ self._event_handlers: Dict[Type[DomainEvent], List[AsyncHandler[Any]]] = {}
38
+ self._command_handlers: Dict[Type[Command], AsyncHandler[Any]] = {}
39
+
40
+ def register_event(self, event_type: Type[DomainEvent], handler: AsyncHandler[Any]) -> None:
41
+ self._event_handlers.setdefault(event_type, []).append(handler)
42
+
43
+ def register_command(self, cmd_type: Type[Command], handler: AsyncHandler[Any]) -> None:
44
+ if cmd_type in self._command_handlers:
45
+ raise ValueError(f"Command handler already registered for {cmd_type!r}")
46
+ self._command_handlers[cmd_type] = handler
47
+
48
+ def for_event(self, event_type: Type[DomainEvent]) -> List[AsyncHandler[Any]]:
49
+ return self._event_handlers.get(event_type, [])
50
+
51
+ def for_command(self, cmd_type: Type[Command]) -> AsyncHandler[Any]:
52
+ try:
53
+ return self._command_handlers[cmd_type]
54
+ except KeyError as e:
55
+ raise KeyError(f"No handler for command {cmd_type.__name__}") from e
56
+
57
+
58
+ def handles_event(event_type: Type[DomainEvent], registry: HandlerRegistry):
59
+ def deco(fn: SyncHandler[Any]) -> SyncHandler[Any]:
60
+ registry.register_event(event_type, fn)
61
+ return fn
62
+
63
+ return deco
64
+
65
+
66
+ def handles_command(cmd_type: Type[Command], registry: HandlerRegistry):
67
+ def deco(fn: SyncHandler[Any]) -> SyncHandler[Any]:
68
+ registry.register_command(cmd_type, fn)
69
+ return fn
70
+
71
+ return deco
72
+
73
+
74
+ def handles_event_async(event_type: Type[DomainEvent], registry: AsyncHandlerRegistry):
75
+ def deco(fn: AsyncHandler[Any]) -> AsyncHandler[Any]:
76
+ registry.register_event(event_type, fn)
77
+ return fn
78
+
79
+ return deco
80
+
81
+
82
+ def handles_command_async(cmd_type: Type[Command], registry: AsyncHandlerRegistry):
83
+ def deco(fn: AsyncHandler[Any]) -> AsyncHandler[Any]:
84
+ registry.register_command(cmd_type, fn)
85
+ return fn
86
+
87
+ return deco
88
+
89
+
90
+ def make_sync_decorators(registry: HandlerRegistry, container: TypeContainer):
91
+ def handles_command(cmd_type: Type[Any]):
92
+ def deco(h: Any):
93
+ registry.register_command(cmd_type, bind_handler(h, container))
94
+ return h
95
+
96
+ return deco
97
+
98
+ def handles_event(evt_type: Type[Any]):
99
+ def deco(h: Any):
100
+ registry.register_event(evt_type, bind_handler(h, container))
101
+ return h
102
+
103
+ return deco
104
+
105
+ return handles_command, handles_event
106
+
107
+
108
+ def make_async_decorators(registry: AsyncHandlerRegistry, container: TypeContainer):
109
+ def handles_command_async(cmd_type: Type[Any]):
110
+ def deco(h: Any):
111
+ registry.register_command(cmd_type, bind_handler(h, container))
112
+ return h
113
+
114
+ return deco
115
+
116
+ def handles_event_async(evt_type: Type[Any]):
117
+ def deco(h: Any):
118
+ registry.register_event(evt_type, bind_handler(h, container))
119
+ return h
120
+
121
+ return deco
122
+
123
+ return handles_command_async, handles_event_async
@@ -0,0 +1,115 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import (
3
+ TypeVar,
4
+ Generic,
5
+ Optional,
6
+ Iterable,
7
+ Callable,
8
+ List,
9
+ ClassVar,
10
+ Type,
11
+ Any,
12
+ )
13
+
14
+ from nlbone.interfaces.api.exceptions import NotFoundException
15
+
16
+ ID = TypeVar("ID")
17
+ T = TypeVar("T", bound="HasId")
18
+
19
+
20
+ class Repository(Generic[T, ID], ABC):
21
+ model: ClassVar[Type[Any]]
22
+
23
+ @abstractmethod
24
+ def get(self, id: ID) -> Optional[T]: ...
25
+
26
+ def get_or_raise(self, id: ID) -> T:
27
+ ...
28
+ entity = self.get(id)
29
+ if entity is None:
30
+ raise NotFoundException(f"Entity with id={id!r} not found")
31
+ return entity
32
+
33
+ @abstractmethod
34
+ def list(
35
+ self,
36
+ *,
37
+ offset: int = 0,
38
+ limit: Optional[int] = None,
39
+ where: Optional[Callable[[T], bool]] = None,
40
+ order_by: Optional[Callable[[T], object]] = None,
41
+ reverse: bool = False,
42
+ ) -> List[T]: ...
43
+
44
+ @abstractmethod
45
+ def count(self, *, where: Optional[Callable[[T], bool]] = None) -> int: ...
46
+
47
+ @abstractmethod
48
+ def exists(self, id: ID) -> bool: ...
49
+
50
+ # --- Write ---
51
+ @abstractmethod
52
+ def add(self, entity: T) -> T: ...
53
+
54
+ @abstractmethod
55
+ def add_many(self, entities: Iterable[T]) -> List[T]: ...
56
+
57
+ @abstractmethod
58
+ def update(self, entity: T) -> T: ...
59
+
60
+ @abstractmethod
61
+ def delete(self, id: ID) -> bool: ...
62
+
63
+ @abstractmethod
64
+ def clear(self) -> None: ...
65
+
66
+
67
+ # -----------------------------
68
+ # Async Repository (Abstract)
69
+ # -----------------------------
70
+ class AsyncRepository(Generic[T, ID], ABC):
71
+ @property
72
+ @abstractmethod
73
+ def model(self) -> type[T]: ...
74
+
75
+ @abstractmethod
76
+ async def get(self, id: ID) -> Optional[T]: ...
77
+
78
+ async def get_or_raise(self, id: ID) -> T:
79
+ entity = await self.get(id)
80
+ if entity is None:
81
+ raise NotFoundException(f"Entity with id={id!r} not found")
82
+ return entity
83
+
84
+ @abstractmethod
85
+ async def list(
86
+ self,
87
+ *,
88
+ offset: int = 0,
89
+ limit: Optional[int] = None,
90
+ where: Optional[Callable[[T], bool]] = None,
91
+ order_by: Optional[Callable[[T], object]] = None,
92
+ reverse: bool = False,
93
+ ) -> List[T]: ...
94
+
95
+ @abstractmethod
96
+ async def count(self, *, where: Optional[Callable[[T], bool]] = None) -> int: ...
97
+
98
+ @abstractmethod
99
+ async def exists(self, id: ID) -> bool: ...
100
+
101
+ # --- Write ---
102
+ @abstractmethod
103
+ async def add(self, entity: T) -> T: ...
104
+
105
+ @abstractmethod
106
+ async def add_many(self, entities: Iterable[T]) -> List[T]: ...
107
+
108
+ @abstractmethod
109
+ async def update(self, entity: T) -> T: ...
110
+
111
+ @abstractmethod
112
+ async def delete(self, id: ID) -> bool: ...
113
+
114
+ @abstractmethod
115
+ async def clear(self) -> None: ...
@@ -0,0 +1,22 @@
1
+ import typer
2
+
3
+ from nlbone.utils.crypto import decrypt_text, encrypt_text
4
+
5
+ crypto_command = typer.Typer(help="Encryption / Decryption utilities")
6
+
7
+
8
+ @crypto_command.command("encrypt")
9
+ def encrypt_cmd(value: str):
10
+ """Encrypt a plain text string."""
11
+ encrypted = encrypt_text(value)
12
+ typer.secho(f"🔐 Encrypted:\n{encrypted}", fg=typer.colors.GREEN)
13
+
14
+
15
+ @crypto_command.command("decrypt")
16
+ def decrypt_cmd(value: str):
17
+ """Decrypt an encrypted token string."""
18
+ try:
19
+ decrypted = decrypt_text(value)
20
+ typer.secho(f"🔓 Decrypted:\n{decrypted}", fg=typer.colors.CYAN)
21
+ except Exception as e:
22
+ typer.secho(f"❌ Failed to decrypt: {e}", fg=typer.colors.RED)
@@ -4,11 +4,13 @@ import typer
4
4
 
5
5
  from nlbone.adapters.db import init_sync_engine
6
6
  from nlbone.config.settings import get_settings
7
+ from nlbone.interfaces.cli.crypto import crypto_command
7
8
  from nlbone.interfaces.cli.init_db import init_db_command
8
9
 
9
10
  app = typer.Typer(help="NLBone CLI")
10
11
 
11
12
  app.add_typer(init_db_command, name="db")
13
+ app.add_typer(crypto_command, name="crypto")
12
14
 
13
15
 
14
16
  @app.callback()
@@ -1,6 +1,7 @@
1
1
  from nlbone.adapters.messaging.internal_router import internal_router
2
2
  from nlbone.core.ports.event_bus import IntegrationPublisher
3
3
 
4
+
4
5
  def run_dispatch_outbox(outbox_repo, publisher: IntegrationPublisher):
5
6
  batch = outbox_repo.fetch_pending(limit=200)
6
7
  for rec in batch:
@@ -0,0 +1,29 @@
1
+ import base64
2
+ import hashlib
3
+
4
+ from cryptography.fernet import Fernet
5
+
6
+ from nlbone.config.settings import get_settings
7
+
8
+
9
+ def _get_fernet_key() -> Fernet:
10
+ settings = get_settings()
11
+ fernet_key = settings.FERNET_KEY
12
+
13
+ if not fernet_key or not fernet_key.strip():
14
+ raise Exception("❌ FERNET_KEY is required in .env")
15
+
16
+ digest = hashlib.sha256(fernet_key.encode()).digest()
17
+
18
+ return base64.urlsafe_b64encode(digest)
19
+
20
+
21
+ fernet = Fernet(_get_fernet_key())
22
+
23
+
24
+ def encrypt_text(plaintext: str) -> str:
25
+ return fernet.encrypt(plaintext.encode()).decode()
26
+
27
+
28
+ def decrypt_text(token: str) -> str:
29
+ return fernet.decrypt(token.encode()).decode()
@@ -0,0 +1,33 @@
1
+ import re
2
+ from typing import List
3
+
4
+
5
+ def normalize_mobile(mobile, strip_zero=True, add_country_code=True):
6
+ if not mobile:
7
+ return ""
8
+
9
+ mobile = re.sub(r"\D", "", str(mobile))
10
+
11
+ if mobile.startswith("0098"):
12
+ mobile = mobile[4:]
13
+ elif mobile.startswith("98") and len(mobile) > 10:
14
+ mobile = mobile[2:]
15
+
16
+ if strip_zero and mobile.startswith("0"):
17
+ mobile = mobile[1:]
18
+
19
+ if add_country_code:
20
+ if not mobile.startswith("98"):
21
+ mobile = f"98{mobile}"
22
+
23
+ return mobile
24
+
25
+
26
+ def remove_duplicates(items: List[str]) -> List[str]:
27
+ seen = set()
28
+ out = []
29
+ for x in items:
30
+ if x and x not in seen:
31
+ seen.add(x)
32
+ out.append(x)
33
+ return out
@@ -1,25 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Protocol, Type, Any, Dict
3
-
4
-
5
- @dataclass(frozen=True)
6
- class Command:
7
- pass
8
-
9
-
10
- class CommandHandler(Protocol):
11
- def __call__(self, command: Command) -> Any: ...
12
-
13
-
14
- class CommandBus:
15
- def __init__(self) -> None:
16
- self._handlers: Dict[Type[Command], CommandHandler] = {}
17
-
18
- def register(self, cmd_type: Type[Command], handler: CommandHandler) -> None:
19
- self._handlers[cmd_type] = handler
20
-
21
- def dispatch(self, command: Command) -> Any:
22
- handler = self._handlers.get(type(command))
23
- if handler is None:
24
- raise LookupError(f"No handler registered for {type(command).__name__}")
25
- return handler(command)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes