nlbone 0.4.0__tar.gz → 0.4.2__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 (89) hide show
  1. {nlbone-0.4.0 → nlbone-0.4.2}/PKG-INFO +5 -9
  2. {nlbone-0.4.0 → nlbone-0.4.2}/pyproject.toml +29 -18
  3. nlbone-0.4.2/src/nlbone/adapters/auth/__init__.py +1 -0
  4. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/auth/keycloak.py +6 -1
  5. nlbone-0.4.2/src/nlbone/adapters/db/__init__.py +4 -0
  6. nlbone-0.4.2/src/nlbone/adapters/db/postgres/__init__.py +4 -0
  7. nlbone-0.4.2/src/nlbone/adapters/db/postgres/audit.py +148 -0
  8. {nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy → nlbone-0.4.2/src/nlbone/adapters/db/postgres}/base.py +0 -2
  9. {nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy → nlbone-0.4.2/src/nlbone/adapters/db/postgres}/engine.py +4 -3
  10. {nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy → nlbone-0.4.2/src/nlbone/adapters/db/postgres}/query_builder.py +27 -11
  11. {nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy → nlbone-0.4.2/src/nlbone/adapters/db/postgres}/repository.py +4 -2
  12. {nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy → nlbone-0.4.2/src/nlbone/adapters/db/postgres}/schema.py +7 -7
  13. {nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy → nlbone-0.4.2/src/nlbone/adapters/db/postgres}/uow.py +3 -2
  14. nlbone-0.4.2/src/nlbone/adapters/db/redis/client.py +22 -0
  15. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/http_clients/uploadchi.py +27 -11
  16. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/http_clients/uploadchi_async.py +29 -7
  17. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/messaging/event_bus.py +3 -0
  18. nlbone-0.4.2/src/nlbone/adapters/percolation/__init__.py +1 -0
  19. nlbone-0.4.2/src/nlbone/adapters/percolation/connection.py +12 -0
  20. nlbone-0.4.2/src/nlbone/config/logging.py +119 -0
  21. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/config/settings.py +20 -24
  22. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/container.py +6 -5
  23. nlbone-0.4.2/src/nlbone/core/application/base_worker.py +36 -0
  24. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/application/events.py +5 -1
  25. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/application/use_case.py +3 -1
  26. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/domain/base.py +4 -0
  27. nlbone-0.4.2/src/nlbone/core/domain/models.py +38 -0
  28. nlbone-0.4.2/src/nlbone/core/ports/__init__.py +5 -0
  29. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/ports/auth.py +1 -0
  30. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/ports/event_bus.py +3 -0
  31. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/ports/files.py +26 -5
  32. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/ports/repo.py +5 -2
  33. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/ports/uow.py +3 -0
  34. nlbone-0.4.2/src/nlbone/interfaces/api/dependencies/__init__.py +11 -0
  35. nlbone-0.4.2/src/nlbone/interfaces/api/dependencies/async_auth.py +61 -0
  36. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/dependencies/auth.py +5 -3
  37. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/dependencies/db.py +5 -3
  38. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/dependencies/uow.py +3 -2
  39. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/exception_handlers.py +17 -15
  40. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/exceptions.py +1 -2
  41. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/middleware/__init__.py +2 -2
  42. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/middleware/access_log.py +12 -8
  43. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/middleware/add_request_context.py +55 -52
  44. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/middleware/authentication.py +4 -1
  45. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/pagination/__init__.py +4 -5
  46. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -2
  47. nlbone-0.4.2/src/nlbone/interfaces/cli/init_db.py +28 -0
  48. nlbone-0.4.2/src/nlbone/interfaces/cli/main.py +29 -0
  49. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/utils/context.py +14 -4
  50. nlbone-0.4.2/src/nlbone/utils/redactor.py +32 -0
  51. nlbone-0.4.2/src/nlbone/utils/time.py +44 -0
  52. nlbone-0.4.0/src/nlbone/adapters/auth/__init__.py +0 -1
  53. nlbone-0.4.0/src/nlbone/adapters/db/__init__.py +0 -3
  54. nlbone-0.4.0/src/nlbone/adapters/db/postgres.py +0 -0
  55. nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy/__init__.py +0 -4
  56. nlbone-0.4.0/src/nlbone/config/logging.py +0 -160
  57. nlbone-0.4.0/src/nlbone/core/domain/models.py +0 -0
  58. nlbone-0.4.0/src/nlbone/core/ports/__init__.py +0 -5
  59. nlbone-0.4.0/src/nlbone/interfaces/api/dependencies/__init__.py +0 -3
  60. nlbone-0.4.0/src/nlbone/interfaces/cli/init_db.py +0 -17
  61. nlbone-0.4.0/src/nlbone/interfaces/cli/main.py +0 -0
  62. nlbone-0.4.0/src/nlbone/utils/time.py +0 -5
  63. {nlbone-0.4.0 → nlbone-0.4.2}/.gitignore +0 -0
  64. {nlbone-0.4.0 → nlbone-0.4.2}/LICENSE +0 -0
  65. {nlbone-0.4.0 → nlbone-0.4.2}/README.md +0 -0
  66. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/__init__.py +0 -0
  67. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/__init__.py +0 -0
  68. {nlbone-0.4.0/src/nlbone/adapters/http_clients → nlbone-0.4.2/src/nlbone/adapters/db/redis}/__init__.py +0 -0
  69. {nlbone-0.4.0/src/nlbone/config → nlbone-0.4.2/src/nlbone/adapters/http_clients}/__init__.py +0 -0
  70. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/http_clients/email_gateway.py +0 -0
  71. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/messaging/__init__.py +0 -0
  72. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/adapters/messaging/redis.py +0 -0
  73. {nlbone-0.4.0/src/nlbone/core → nlbone-0.4.2/src/nlbone/config}/__init__.py +0 -0
  74. {nlbone-0.4.0/src/nlbone/core/application → nlbone-0.4.2/src/nlbone/core}/__init__.py +0 -0
  75. {nlbone-0.4.0/src/nlbone/core/application/services → nlbone-0.4.2/src/nlbone/core/application}/__init__.py +0 -0
  76. {nlbone-0.4.0/src/nlbone/core/domain → nlbone-0.4.2/src/nlbone/core/application/services}/__init__.py +0 -0
  77. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/application/services.py +0 -0
  78. {nlbone-0.4.0/src/nlbone/interfaces → nlbone-0.4.2/src/nlbone/core/domain}/__init__.py +0 -0
  79. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/domain/events.py +0 -0
  80. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/core/ports/messaging.py +0 -0
  81. {nlbone-0.4.0/src/nlbone/interfaces/api → nlbone-0.4.2/src/nlbone/interfaces}/__init__.py +0 -0
  82. {nlbone-0.4.0/src/nlbone/interfaces/cli → nlbone-0.4.2/src/nlbone/interfaces/api}/__init__.py +0 -0
  83. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/routers.py +0 -0
  84. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/api/schemas.py +0 -0
  85. {nlbone-0.4.0/src/nlbone/interfaces/jobs → nlbone-0.4.2/src/nlbone/interfaces/cli}/__init__.py +0 -0
  86. {nlbone-0.4.0/src/nlbone/utils → nlbone-0.4.2/src/nlbone/interfaces/jobs}/__init__.py +0 -0
  87. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  88. {nlbone-0.4.0 → nlbone-0.4.2}/src/nlbone/types.py +0 -0
  89. /nlbone-0.4.0/src/nlbone/adapters/db/memory.py → /nlbone-0.4.2/src/nlbone/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.4.0
3
+ Version: 0.4.2
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,23 +8,19 @@ License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: anyio>=4.0
10
10
  Requires-Dist: dependency-injector>=4.48.1
11
+ Requires-Dist: elasticsearch==8.14.0
11
12
  Requires-Dist: fastapi>=0.116
12
13
  Requires-Dist: httpx>=0.27
13
14
  Requires-Dist: psycopg>=3.2.9
14
15
  Requires-Dist: pydantic-settings>=2.0
15
16
  Requires-Dist: pydantic>=2.0
17
+ Requires-Dist: python-dateutil~=2.9.0.post0
16
18
  Requires-Dist: python-keycloak==5.8.1
19
+ Requires-Dist: redis~=6.4.0
17
20
  Requires-Dist: sqlalchemy>=2.0
18
21
  Requires-Dist: starlette>=0.47
22
+ Requires-Dist: typer>=0.17.4
19
23
  Requires-Dist: uvicorn>=0.35
20
- Provides-Extra: dev
21
- Requires-Dist: mypy>=1.10; extra == 'dev'
22
- Requires-Dist: pre-commit>=3.7; extra == 'dev'
23
- Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
24
- Requires-Dist: pytest>=8.0; extra == 'dev'
25
- Requires-Dist: ruff>=0.5; extra == 'dev'
26
- Requires-Dist: tomli; extra == 'dev'
27
- Requires-Dist: twine; extra == 'dev'
28
24
  Description-Content-Type: text/markdown
29
25
 
30
26
  # nlbone
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlbone"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Backbone package for interfaces and infrastructure in Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -22,23 +22,24 @@ dependencies = [
22
22
  "uvicorn>=0.35",
23
23
  "sqlalchemy>=2.0",
24
24
  "psycopg>=3.2.9",
25
- "dependency-injector>=4.48.1"
26
- ]
27
-
28
- [project.optional-dependencies]
29
- dev = [
30
- "pytest>=8.0",
31
- "pytest-asyncio>=0.23",
32
- "ruff>=0.5",
33
- "mypy>=1.10",
34
- "pre-commit>=3.7",
35
- "twine",
36
- "tomli"
25
+ "dependency-injector>=4.48.1",
26
+ "elasticsearch==8.14.0",
27
+ "redis~=6.4.0",
28
+ "python-dateutil~=2.9.0.post0",
29
+ "typer>=0.17.4"
37
30
  ]
38
31
 
39
32
  [tool.ruff]
40
- line-length = 100
41
- target-version = "py310"
33
+ line-length = 120
34
+ target-version = "py312"
35
+ exclude = ["tests"]
36
+
37
+ [tool.ruff.lint]
38
+ select = ["E", "F", "I"]
39
+ preview = true
40
+
41
+ [tool.ruff.lint.per-file-ignores]
42
+ "__init__.py" = ["F401"]
42
43
 
43
44
  [tool.pytest.ini_options]
44
45
  asyncio_mode = "auto"
@@ -48,7 +49,17 @@ packages = ["src/nlbone"]
48
49
 
49
50
  [tool.hatch.build.targets.sdist]
50
51
  include = [
51
- "src/nlbone",
52
- "README.md",
53
- "LICENSE",
52
+ "src/nlbone",
53
+ "README.md",
54
+ "LICENSE",
54
55
  ]
56
+
57
+ [dependency-groups]
58
+ dev = [
59
+ "pre-commit>=4.3.0",
60
+ "pytest>=8.4.2",
61
+ "ruff>=0.12.12",
62
+ ]
63
+
64
+ [project.scripts]
65
+ nlbone = "nlbone.interfaces.cli.main:main"
@@ -0,0 +1 @@
1
+ from .keycloak import KeycloakAuthService
@@ -1,7 +1,8 @@
1
1
  from keycloak import KeycloakOpenID
2
2
  from keycloak.exceptions import KeycloakAuthenticationError
3
+
4
+ from nlbone.config.settings import Settings, get_settings, is_production_env
3
5
  from nlbone.core.ports.auth import AuthService
4
- from nlbone.config.settings import Settings, get_settings
5
6
 
6
7
 
7
8
  class KeycloakAuthService(AuthService):
@@ -13,8 +14,12 @@ class KeycloakAuthService(AuthService):
13
14
  realm_name=s.KEYCLOAK_REALM_NAME,
14
15
  client_secret_key=s.KEYCLOAK_CLIENT_SECRET.get_secret_value().strip(),
15
16
  )
17
+ self.bypass = not is_production_env()
16
18
 
17
19
  def has_access(self, token, permissions):
20
+ if self.bypass:
21
+ return True
22
+
18
23
  try:
19
24
  result = self.keycloak_openid.has_uma_access(token, permissions=permissions)
20
25
  return result.is_authorized
@@ -0,0 +1,4 @@
1
+ from .postgres import apply_pagination, get_paginated_response
2
+ from .postgres.base import Base
3
+ from .postgres.engine import async_ping, async_session, init_async_engine, init_sync_engine, sync_ping, sync_session
4
+ import nlbone.adapters.db.postgres.audit
@@ -0,0 +1,4 @@
1
+ from .engine import async_ping, async_session, init_async_engine, init_sync_engine, sync_ping, sync_session
2
+ from .query_builder import apply_pagination, get_paginated_response
3
+ from .repository import AsyncSqlAlchemyRepository, SqlAlchemyRepository
4
+ from .uow import AsyncSqlAlchemyUnitOfWork, SqlAlchemyUnitOfWork
@@ -0,0 +1,148 @@
1
+ import uuid
2
+ from datetime import date, datetime
3
+ from typing import Any
4
+ from sqlalchemy import event, inspect as sa_inspect
5
+ from sqlalchemy.orm import Session as SASession
6
+ from enum import Enum as _Enum
7
+ from decimal import Decimal
8
+
9
+ from nlbone.core.domain.models import AuditLog
10
+ from nlbone.utils.context import current_context_dict
11
+
12
+ DEFAULT_EXCLUDE = {"updated_at", "created_at"}
13
+ DEFAULT_ENABLED = False
14
+ DEFAULT_OPS = {"INSERT", "UPDATE", "DELETE"}
15
+
16
+
17
+ def _get_ops_for(obj) -> set[str]:
18
+ ops = getattr(obj, "__audit_ops__", None)
19
+ if ops is None:
20
+ return set(DEFAULT_OPS)
21
+ return {str(op).upper() for op in ops}
22
+
23
+
24
+ def _is_audit_disabled(obj) -> bool:
25
+ if getattr(obj, "__audit_disable__", False):
26
+ return True
27
+ if hasattr(obj, "__audit_enable__") and not getattr(obj, "__audit_enable__"):
28
+ return True
29
+ return False
30
+
31
+
32
+ def _is_op_enabled(obj, op: str) -> bool:
33
+ if _is_audit_disabled(obj):
34
+ return False
35
+ return op.upper() in _get_ops_for(obj)
36
+
37
+
38
+ def _ser(val):
39
+ if isinstance(val, (date, datetime)):
40
+ return val.isoformat()
41
+ # UUID
42
+ if isinstance(val, uuid.UUID):
43
+ return str(val)
44
+ # Enum
45
+ if isinstance(val, _Enum):
46
+ return val.value
47
+ if isinstance(val, Decimal):
48
+ return str(val)
49
+ if isinstance(val, set):
50
+ return list(val)
51
+ return val
52
+
53
+
54
+ def _entity_name(obj: Any) -> str:
55
+ return getattr(getattr(obj, "__table__", None), "name", None) or getattr(obj, "__tablename__",
56
+ None) or obj.__class__.__name__
57
+
58
+
59
+ def _entity_id(obj: Any) -> str:
60
+ insp = sa_inspect(obj)
61
+ if insp.identity and len(insp.identity) == 1:
62
+ return _ser(insp.identity[0])
63
+ for pk in insp.mapper.primary_key:
64
+ v = getattr(obj, pk.key)
65
+ if v is not None:
66
+ return _ser(v)
67
+ return _ser(getattr(obj, "id", "?"))
68
+
69
+
70
+ def _changes_for_update(obj: any) -> dict[str, dict[str, any]]:
71
+ changes = {}
72
+ insp = sa_inspect(obj)
73
+ exclude = set(getattr(obj, "__audit_exclude__", set())) | DEFAULT_EXCLUDE
74
+
75
+ for col in insp.mapper.column_attrs:
76
+ key = col.key
77
+ if key in exclude:
78
+ continue
79
+
80
+ try:
81
+ state = insp.attrs[key]
82
+ except KeyError:
83
+ continue
84
+
85
+ hist = state.history # History object
86
+ if hist.has_changes():
87
+ old = hist.deleted[0] if hist.deleted else None
88
+ new = hist.added[0] if hist.added else None
89
+ if old != new:
90
+ changes[key] = {"old": _ser(old), "new": _ser(new)}
91
+ return changes
92
+ @event.listens_for(SASession, "before_flush")
93
+ def before_flush(session: SASession, flush_context, instances):
94
+ entries = session.info.setdefault("_audit_entries", [])
95
+
96
+ # INSERT
97
+ for obj in session.new:
98
+ if isinstance(obj, AuditLog) or not _is_op_enabled(obj, "INSERT"):
99
+ continue
100
+ insp = sa_inspect(obj)
101
+ exclude = set(getattr(obj, "__audit_exclude__", set())) | DEFAULT_EXCLUDE
102
+ row = {}
103
+ for col_attr in insp.mapper.column_attrs:
104
+ key = col_attr.key
105
+ if key in exclude:
106
+ continue
107
+ row[key] = _ser(getattr(obj, key, None))
108
+ entries.append({
109
+ "obj": obj,
110
+ "op": "INSERT",
111
+ "changes": {k: {"old": None, "new": v} for k, v in row.items()}
112
+ })
113
+
114
+ # UPDATE
115
+ for obj in session.dirty:
116
+ if isinstance(obj, AuditLog) or not _is_op_enabled(obj, "UPDATE"):
117
+ continue
118
+ if session.is_modified(obj, include_collections=False):
119
+ ch = _changes_for_update(obj)
120
+ if ch:
121
+ entries.append({"obj": obj, "op": "UPDATE", "changes": ch})
122
+
123
+ # DELETE
124
+ for obj in session.deleted:
125
+ if isinstance(obj, AuditLog) or not _is_op_enabled(obj, "DELETE"):
126
+ continue
127
+ entries.append({"obj": obj, "op": "DELETE", "changes": None})
128
+
129
+
130
+ @event.listens_for(SASession, "after_flush_postexec")
131
+ def after_flush_postexec(session: SASession, flush_context):
132
+ entries = session.info.pop("_audit_entries", [])
133
+ if not entries:
134
+ return
135
+ ctx = current_context_dict()
136
+ for e in entries:
137
+ obj = e["obj"]
138
+ al = AuditLog(
139
+ entity=_entity_name(obj),
140
+ entity_id=str(_entity_id(obj)),
141
+ operation=e["op"],
142
+ changes=e.get("changes"),
143
+ actor_id=ctx.get("user_id"),
144
+ request_id=ctx.get("request_id"),
145
+ ip=ctx.get("ip"),
146
+ user_agent=ctx.get("user_agent"),
147
+ )
148
+ session.add(al)
@@ -1,5 +1,3 @@
1
1
  from sqlalchemy.orm import declarative_base
2
2
 
3
3
  Base = declarative_base()
4
-
5
-
@@ -1,16 +1,15 @@
1
1
  from contextlib import asynccontextmanager, contextmanager
2
- from typing import Generator, Optional, Any, AsyncGenerator
2
+ from typing import Any, AsyncGenerator, Generator, Optional
3
3
 
4
4
  from sqlalchemy import create_engine, text
5
5
  from sqlalchemy.engine import Engine
6
- from sqlalchemy.orm import Session, sessionmaker
7
-
8
6
  from sqlalchemy.ext.asyncio import (
9
7
  AsyncEngine,
10
8
  AsyncSession,
11
9
  async_sessionmaker,
12
10
  create_async_engine,
13
11
  )
12
+ from sqlalchemy.orm import Session, sessionmaker
14
13
 
15
14
  from nlbone.config.settings import get_settings
16
15
 
@@ -117,12 +116,14 @@ def sync_ping() -> None:
117
116
  with eng.connect() as conn:
118
117
  conn.execute(text("SELECT 1"))
119
118
 
119
+
120
120
  def get_async_session_factory() -> async_sessionmaker[AsyncSession]:
121
121
  if _async_session_factory is None:
122
122
  init_async_engine()
123
123
  assert _async_session_factory is not None
124
124
  return _async_session_factory
125
125
 
126
+
126
127
  def get_sync_session_factory() -> sessionmaker[Session]:
127
128
  if _sync_session_factory is None:
128
129
  init_sync_engine()
@@ -1,12 +1,22 @@
1
- from typing import Union, Callable, Any, Optional, Type, Sequence
1
+ from typing import Any, Callable, Optional, Sequence, Type, Union
2
2
 
3
3
  from sqlalchemy import asc, desc, or_
4
+ from sqlalchemy.dialects.postgresql import ENUM as PGEnum
5
+ from sqlalchemy.orm import Query, Session
4
6
  from sqlalchemy.orm.interfaces import LoaderOption
5
7
  from sqlalchemy.sql.sqltypes import (
6
- String, Text, Integer, BigInteger, SmallInteger, Numeric, Float, Boolean, Enum as SAEnum
8
+ BigInteger,
9
+ Boolean,
10
+ Float,
11
+ Integer,
12
+ Numeric,
13
+ SmallInteger,
14
+ String,
15
+ Text,
16
+ )
17
+ from sqlalchemy.sql.sqltypes import (
18
+ Enum as SAEnum,
7
19
  )
8
- from sqlalchemy.orm import Session, Query
9
- from sqlalchemy.dialects.postgresql import ENUM as PGEnum
10
20
 
11
21
  from nlbone.interfaces.api.exceptions import UnprocessableEntityException
12
22
  from nlbone.interfaces.api.pagination import PaginateRequest, PaginateResponse
@@ -122,12 +132,16 @@ def _apply_filters(pagination, entity, query):
122
132
  return float(v)
123
133
  # Booleans
124
134
  if isinstance(coltype, Boolean):
125
- if isinstance(v, bool): return v
126
- if isinstance(v, (int, float)): return bool(v)
135
+ if isinstance(v, bool):
136
+ return v
137
+ if isinstance(v, (int, float)):
138
+ return bool(v)
127
139
  if isinstance(v, str):
128
140
  vl = v.strip().lower()
129
- if vl in {"true", "1", "yes", "y", "t"}: return True
130
- if vl in {"false", "0", "no", "n", "f"}: return False
141
+ if vl in {"true", "1", "yes", "y", "t"}:
142
+ return True
143
+ if vl in {"false", "0", "no", "n", "f"}:
144
+ return False
131
145
  return None
132
146
  # fallback
133
147
  return v
@@ -215,7 +229,8 @@ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
215
229
  try:
216
230
  obj = output_cls(item) # type: ignore[call-arg]
217
231
  try:
218
- from dataclasses import is_dataclass, asdict
232
+ from dataclasses import asdict, is_dataclass
233
+
219
234
  if is_dataclass(obj):
220
235
  return asdict(obj)
221
236
  except Exception:
@@ -252,5 +267,6 @@ def get_paginated_response(
252
267
  data = [output_cls.model_validate(r, from_attributes=True).model_dump() for r in rows]
253
268
  else:
254
269
  data = rows
255
- return PaginateResponse(total_count=total_count, data=data, limit=pagination.limit,
256
- offset=pagination.offset).to_dict()
270
+ return PaginateResponse(
271
+ total_count=total_count, data=data, limit=pagination.limit, offset=pagination.offset
272
+ ).to_dict()
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
- from typing import Generic, Iterable, Optional, Type, TypeVar, List
2
+
3
+ from typing import Generic, Iterable, List, Optional, Type, TypeVar
3
4
 
4
5
  from sqlalchemy import select
5
6
  from sqlalchemy.ext.asyncio import AsyncSession
6
7
  from sqlalchemy.orm import Session
7
- from nlbone.core.ports.repo import Repository, AsyncRepository
8
+
9
+ from nlbone.core.ports.repo import AsyncRepository, Repository
8
10
 
9
11
  T = TypeVar("T")
10
12
 
@@ -1,18 +1,17 @@
1
-
2
1
  import importlib
3
2
  from typing import Sequence
4
3
 
5
- from nlbone.adapters.db.sqlalchemy.base import Base
6
- from nlbone.adapters.db.sqlalchemy.engine import init_async_engine, init_sync_engine
4
+ from nlbone.adapters.db.postgres.base import Base
5
+ from nlbone.adapters.db.postgres.engine import init_async_engine, init_sync_engine
7
6
 
7
+ DEFAULT_MODEL_MODULES: Sequence[str] = ()
8
8
 
9
- DEFAULT_MODEL_MODULES: Sequence[str] = (
10
- )
11
9
 
12
10
  def import_model_modules(modules: Sequence[str] | None = None) -> None:
13
- for m in (modules or DEFAULT_MODEL_MODULES):
11
+ for m in modules or DEFAULT_MODEL_MODULES:
14
12
  importlib.import_module(m)
15
13
 
14
+
16
15
  # --------- Async (SQLAlchemy 2.x) ----------
17
16
  async def init_db_async(model_modules: Sequence[str] | None = None) -> None:
18
17
  """Create tables using AsyncEngine (dev/test). Prefer Alembic in prod."""
@@ -21,9 +20,10 @@ async def init_db_async(model_modules: Sequence[str] | None = None) -> None:
21
20
  async with engine.begin() as conn:
22
21
  await conn.run_sync(Base.metadata.create_all)
23
22
 
23
+
24
24
  # --------- Sync ----------
25
25
  def init_db_sync(model_modules: Sequence[str] | None = None) -> None:
26
26
  """Create tables using Sync Engine (dev/test). Prefer Alembic in prod."""
27
27
  import_model_modules(model_modules)
28
28
  engine = init_sync_engine()
29
- Base.metadata.create_all(bind=engine)
29
+ Base.metadata.create_all(bind=engine)
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
5
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
6
6
  from sqlalchemy.orm import Session, sessionmaker
7
- from nlbone.core.ports.uow import UnitOfWork
7
+
8
8
  from nlbone.core.ports.uow import AsyncUnitOfWork as AsyncUnitOfWorkPort
9
+ from nlbone.core.ports.uow import UnitOfWork
9
10
 
10
11
 
11
12
  class SqlAlchemyUnitOfWork(UnitOfWork):
@@ -0,0 +1,22 @@
1
+ import redis
2
+
3
+ from nlbone.config.settings import get_settings
4
+
5
+
6
+ class RedisClient:
7
+ _client: redis.Redis | None = None
8
+
9
+ @classmethod
10
+ def get_client(cls) -> redis.Redis:
11
+ if cls._client is None:
12
+ cls._client = redis.from_url(
13
+ get_settings().REDIS_URL,
14
+ decode_responses=True
15
+ )
16
+ return cls._client
17
+
18
+ @classmethod
19
+ def close(cls):
20
+ if cls._client is not None:
21
+ cls._client.close()
22
+ cls._client = None
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import json
3
4
  from typing import Any, Optional
4
5
  from urllib.parse import urlparse, urlunparse
@@ -6,8 +7,8 @@ from urllib.parse import urlparse, urlunparse
6
7
  import httpx
7
8
  import requests
8
9
 
9
- from nlbone.core.ports.files import FileServicePort
10
10
  from nlbone.config.settings import get_settings
11
+ from nlbone.core.ports.files import FileServicePort
11
12
 
12
13
 
13
14
  class UploadchiError(RuntimeError):
@@ -28,8 +29,9 @@ def _auth_headers(token: str | None) -> dict[str, str]:
28
29
  return {"Authorization": f"Bearer {token}"} if token else {}
29
30
 
30
31
 
31
- def _build_list_query(limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None) -> \
32
- dict[str, Any]:
32
+ def _build_list_query(
33
+ limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
34
+ ) -> dict[str, Any]:
33
35
  q: dict[str, Any] = {"limit": limit, "offset": offset}
34
36
  if filters:
35
37
  q["filters"] = json.dumps(filters)
@@ -55,8 +57,12 @@ def _normalize_https_base(url: str) -> str:
55
57
 
56
58
 
57
59
  class UploadchiClient(FileServicePort):
58
- def __init__(self, base_url: Optional[str] = None, timeout_seconds: Optional[float] = None,
59
- client: httpx.Client | None = None) -> None:
60
+ def __init__(
61
+ self,
62
+ base_url: Optional[str] = None,
63
+ timeout_seconds: Optional[float] = None,
64
+ client: httpx.Client | None = None,
65
+ ) -> None:
60
66
  s = get_settings()
61
67
  self._base_url = _normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
62
68
  self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
@@ -65,8 +71,9 @@ class UploadchiClient(FileServicePort):
65
71
  def close(self) -> None:
66
72
  self._client.close()
67
73
 
68
- def upload_file(self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None,
69
- token: str | None = None) -> dict:
74
+ def upload_file(
75
+ self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
76
+ ) -> dict:
70
77
  tok = _resolve_token(token)
71
78
  files = {"file": (filename, file_bytes)}
72
79
  data = (params or {}).copy()
@@ -77,13 +84,22 @@ class UploadchiClient(FileServicePort):
77
84
 
78
85
  def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
79
86
  tok = _resolve_token(token)
80
- r = self._client.post(f"{self._base_url}/{file_id}/commit", headers=_auth_headers(tok),
81
- params={"client_id": client_id} if client_id else None)
87
+ r = self._client.post(
88
+ f"{self._base_url}/{file_id}/commit",
89
+ headers=_auth_headers(tok),
90
+ params={"client_id": client_id} if client_id else None,
91
+ )
82
92
  if r.status_code not in (204, 200):
83
93
  raise UploadchiError(r.status_code, r.text)
84
94
 
85
- def list_files(self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None,
86
- sort: list[tuple[str, str]] | None = None, token: str | None = None) -> dict:
95
+ def list_files(
96
+ self,
97
+ limit: int = 10,
98
+ offset: int = 0,
99
+ filters: dict[str, Any] | None = None,
100
+ sort: list[tuple[str, str]] | None = None,
101
+ token: str | None = None,
102
+ ) -> dict:
87
103
  tok = _resolve_token(token)
88
104
  q = _build_list_query(limit, offset, filters, sort)
89
105
  r = self._client.get(self._base_url, params=q, headers=_auth_headers(tok))
@@ -1,22 +1,35 @@
1
1
  from __future__ import annotations
2
- from typing import Any, Optional, AsyncIterator
2
+
3
+ from typing import Any, AsyncIterator, Optional
4
+
3
5
  import httpx
4
6
 
5
- from nlbone.core.ports.files import AsyncFileServicePort
6
7
  from nlbone.config.settings import get_settings
8
+ from nlbone.core.ports.files import AsyncFileServicePort
9
+
7
10
  from .uploadchi import UploadchiError, _auth_headers, _build_list_query, _filename_from_cd, _resolve_token
8
11
 
12
+
9
13
  class UploadchiAsyncClient(AsyncFileServicePort):
10
- def __init__(self, base_url: Optional[str] = None, timeout_seconds: Optional[float] = None, client: httpx.AsyncClient | None = None) -> None:
14
+ def __init__(
15
+ self,
16
+ base_url: Optional[str] = None,
17
+ timeout_seconds: Optional[float] = None,
18
+ client: httpx.AsyncClient | None = None,
19
+ ) -> None:
11
20
  s = get_settings()
12
21
  self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
13
22
  self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
14
- self._client = client or httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout, follow_redirects=True)
23
+ self._client = client or httpx.AsyncClient(
24
+ base_url=self._base_url, timeout=self._timeout, follow_redirects=True
25
+ )
15
26
 
16
27
  async def aclose(self) -> None:
17
28
  await self._client.aclose()
18
29
 
19
- async def upload_file(self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None) -> dict:
30
+ async def upload_file(
31
+ self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
32
+ ) -> dict:
20
33
  tok = _resolve_token(token)
21
34
  files = {"file": (filename, file_bytes)}
22
35
  data = (params or {}).copy()
@@ -27,11 +40,20 @@ class UploadchiAsyncClient(AsyncFileServicePort):
27
40
 
28
41
  async def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
29
42
  tok = _resolve_token(token)
30
- r = await self._client.post(f"/{file_id}/commit", headers=_auth_headers(tok), params={"client_id": client_id} if client_id else None)
43
+ r = await self._client.post(
44
+ f"/{file_id}/commit", headers=_auth_headers(tok), params={"client_id": client_id} if client_id else None
45
+ )
31
46
  if r.status_code not in (204, 200):
32
47
  raise UploadchiError(r.status_code, await r.aread())
33
48
 
34
- async def list_files(self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None, sort: list[tuple[str, str]] | None = None, token: str | None = None) -> dict:
49
+ async def list_files(
50
+ self,
51
+ limit: int = 10,
52
+ offset: int = 0,
53
+ filters: dict[str, Any] | None = None,
54
+ sort: list[tuple[str, str]] | None = None,
55
+ token: str | None = None,
56
+ ) -> dict:
35
57
  tok = _resolve_token(token)
36
58
  q = _build_list_query(limit, offset, filters, sort)
37
59
  r = await self._client.get("", params=q, headers=_auth_headers(tok))
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
+
2
3
  from collections import defaultdict
3
4
  from typing import Callable, Dict, Iterable, List
5
+
4
6
  from nlbone.core.domain.base import DomainEvent
5
7
  from nlbone.core.ports.event_bus import EventBusPort
6
8
 
9
+
7
10
  class InMemoryEventBus(EventBusPort):
8
11
  def __init__(self) -> None:
9
12
  self._handlers: Dict[str, List[Callable[[DomainEvent], None]]] = defaultdict(list)
@@ -0,0 +1 @@
1
+ from .connection import get_es_client
@@ -0,0 +1,12 @@
1
+ from elasticsearch import Elasticsearch
2
+
3
+ from nlbone.config.settings import get_settings
4
+
5
+ setting = get_settings()
6
+
7
+ def get_es_client():
8
+ es = Elasticsearch(
9
+ setting.ELASTIC_PERCOLATE_URL,
10
+ basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip())
11
+ )
12
+ return es