nlbone 0.7.31__tar.gz → 0.8.0__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 (126) hide show
  1. {nlbone-0.7.31 → nlbone-0.8.0}/PKG-INFO +1 -1
  2. {nlbone-0.7.31 → nlbone-0.8.0}/pyproject.toml +1 -1
  3. nlbone-0.8.0/src/nlbone/adapters/i18n/engine.py +40 -0
  4. nlbone-0.8.0/src/nlbone/adapters/i18n/locales/fa-IR.json +5 -0
  5. nlbone-0.8.0/src/nlbone/interfaces/api/exception_handlers.py +166 -0
  6. nlbone-0.8.0/src/nlbone/interfaces/api/exceptions.py +153 -0
  7. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/schemas.py +1 -0
  8. nlbone-0.8.0/src/nlbone/utils/flatten_dict.py +9 -0
  9. nlbone-0.7.31/src/nlbone/adapters/i18n/engine.py +0 -35
  10. nlbone-0.7.31/src/nlbone/adapters/i18n/locales/fa-IR.json +0 -10
  11. nlbone-0.7.31/src/nlbone/interfaces/api/exception_handlers.py +0 -114
  12. nlbone-0.7.31/src/nlbone/interfaces/api/exceptions.py +0 -108
  13. {nlbone-0.7.31 → nlbone-0.8.0}/.gitignore +0 -0
  14. {nlbone-0.7.31 → nlbone-0.8.0}/LICENSE +0 -0
  15. {nlbone-0.7.31 → nlbone-0.8.0}/README.md +0 -0
  16. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/__init__.py +0 -0
  17. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/__init__.py +0 -0
  18. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/auth/__init__.py +0 -0
  19. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/auth/auth_service.py +0 -0
  20. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/auth/keycloak.py +0 -0
  21. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/auth/token_provider.py +0 -0
  22. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/cache/__init__.py +0 -0
  23. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/cache/async_redis.py +0 -0
  24. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/cache/memory.py +0 -0
  25. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/cache/pubsub_listener.py +0 -0
  26. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/cache/redis.py +0 -0
  27. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/__init__.py +0 -0
  28. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
  29. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/audit.py +0 -0
  30. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/base.py +0 -0
  31. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/engine.py +0 -0
  32. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/query_builder.py +0 -0
  33. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/repository.py +0 -0
  34. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/schema.py +0 -0
  35. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/types.py +0 -0
  36. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/postgres/uow.py +0 -0
  37. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/redis/__init__.py +0 -0
  38. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/db/redis/client.py +0 -0
  39. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/http_clients/__init__.py +0 -0
  40. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/http_clients/pricing/__init__.py +0 -0
  41. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/http_clients/pricing/pricing_service.py +0 -0
  42. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/http_clients/uploadchi/__init__.py +0 -0
  43. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/http_clients/uploadchi/uploadchi.py +0 -0
  44. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +0 -0
  45. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/i18n/__init__.py +0 -0
  46. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/i18n/loaders.py +0 -0
  47. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/messaging/__init__.py +0 -0
  48. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/messaging/event_bus.py +0 -0
  49. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/messaging/rabbitmq.py +0 -0
  50. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/outbox/__init__.py +0 -0
  51. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/outbox/outbox_consumer.py +0 -0
  52. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/outbox/outbox_repo.py +0 -0
  53. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/percolation/__init__.py +0 -0
  54. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/percolation/connection.py +0 -0
  55. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/repositories/__init__.py +0 -0
  56. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/snowflake.py +0 -0
  57. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/ticketing/__init__.py +0 -0
  58. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/adapters/ticketing/client.py +0 -0
  59. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/config/__init__.py +0 -0
  60. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/config/logging.py +0 -0
  61. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/config/settings.py +0 -0
  62. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/container.py +0 -0
  63. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/__init__.py +0 -0
  64. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/__init__.py +0 -0
  65. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/base_worker.py +0 -0
  66. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/bus.py +0 -0
  67. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/di.py +0 -0
  68. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/registry.py +0 -0
  69. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/services/__init__.py +0 -0
  70. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/application/use_case.py +0 -0
  71. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/domain/__init__.py +0 -0
  72. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/domain/base.py +0 -0
  73. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/domain/models.py +0 -0
  74. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/__init__.py +0 -0
  75. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/auth.py +0 -0
  76. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/cache.py +0 -0
  77. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/event_bus.py +0 -0
  78. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/files.py +0 -0
  79. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/outbox.py +0 -0
  80. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/repository.py +0 -0
  81. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/translation.py +0 -0
  82. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/core/ports/uow.py +0 -0
  83. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/__init__.py +0 -0
  84. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/__init__.py +0 -0
  85. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/additional_filed/__init__.py +0 -0
  86. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/additional_filed/assembler.py +0 -0
  87. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py +0 -0
  88. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py +0 -0
  89. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/additional_filed/field_registry.py +0 -0
  90. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/additional_filed/resolver.py +0 -0
  91. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
  92. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -0
  93. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/dependencies/auth.py +0 -0
  94. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/dependencies/client_credential.py +0 -0
  95. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
  96. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
  97. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
  98. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
  99. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
  100. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
  101. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
  102. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
  103. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/routers.py +0 -0
  104. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/schema/__init__.py +0 -0
  105. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/schema/adaptive_schema.py +0 -0
  106. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/api/schema/base_response_model.py +0 -0
  107. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/cli/__init__.py +0 -0
  108. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/cli/crypto.py +0 -0
  109. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/cli/init_db.py +0 -0
  110. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/cli/main.py +0 -0
  111. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/cli/ticket.py +0 -0
  112. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/jobs/__init__.py +0 -0
  113. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/jobs/dispatch_outbox.py +0 -0
  114. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  115. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/types.py +0 -0
  116. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/__init__.py +0 -0
  117. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/cache.py +0 -0
  118. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/cache_keys.py +0 -0
  119. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/cache_registry.py +0 -0
  120. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/context.py +0 -0
  121. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/crypto.py +0 -0
  122. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/flatten_sqlalchemy_result.py +0 -0
  123. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/http.py +0 -0
  124. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/normalize_mobile.py +0 -0
  125. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/redactor.py +0 -0
  126. {nlbone-0.7.31 → nlbone-0.8.0}/src/nlbone/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.7.31
3
+ Version: 0.8.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,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlbone"
7
- version = "0.7.31"
7
+ version = "0.8.0"
8
8
  description = "Backbone package for interfaces and infrastructure in Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,40 @@
1
+ import logging
2
+ from typing import Dict, Optional
3
+
4
+ from nlbone.adapters.i18n.loaders import BaseLoader
5
+
6
+ from nlbone.core.ports.translation import TranslationPort
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class I18nAdapter(TranslationPort):
12
+ def __init__(self, loader: BaseLoader, default_locale: str = "fa-IR"):
13
+ self.default_locale = default_locale
14
+ self.loader = loader
15
+ self._translations: Optional[Dict[str, Dict[str, str]]] = None
16
+
17
+ def _ensure_loaded(self):
18
+ if self._translations is None:
19
+ self._translations = self.loader.load()
20
+
21
+ def translate(self, key: str, locale: Optional[str] = None, **kwargs) -> str:
22
+ target_locale = locale or self.default_locale
23
+ try:
24
+ self._ensure_loaded()
25
+
26
+ locale_data = self._translations.get(target_locale, {})
27
+ text = locale_data.get(key)
28
+
29
+ if text is None:
30
+ text = self._translations.get(self.default_locale, {}).get(key, key)
31
+
32
+ if kwargs:
33
+ try:
34
+ return text.format(**kwargs)
35
+ except KeyError:
36
+ pass
37
+ return text
38
+ except:
39
+ logger.exception("Failed to translate key '{}' to locale '{}'".format(key, target_locale))
40
+ return key
@@ -0,0 +1,5 @@
1
+ {
2
+ "auth": {
3
+ "otp_sent": "کد تایید برای {mobile} ارسال شد."
4
+ }
5
+ }
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Optional, List
4
+ from uuid import uuid4
5
+
6
+ from fastapi import FastAPI, Request
7
+ from fastapi import HTTPException as FastAPIHTTPException
8
+ from fastapi.exceptions import RequestValidationError
9
+ from fastapi.responses import JSONResponse
10
+ from pydantic import BaseModel, ValidationError
11
+ from starlette.exceptions import HTTPException as StarletteHTTPException
12
+ from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR, HTTP_422_UNPROCESSABLE_ENTITY
13
+
14
+ from nlbone.adapters.i18n import translator as _
15
+ from .exceptions import BaseHttpException, ErrorDetail, UnprocessableEntityException
16
+
17
+
18
+ class ErrorResponse(BaseModel):
19
+ message: str
20
+ errors: List[ErrorDetail]
21
+
22
+
23
+ def _ensure_trace_id(request: Request) -> str:
24
+ rid = request.headers.get("X-Request-Id") or request.headers.get("X-Trace-Id")
25
+ return rid or str(uuid4())
26
+
27
+
28
+ def _json_response(
29
+ request: Request,
30
+ status_code: int,
31
+ content: Any,
32
+ trace_id: Optional[str] = None,
33
+ headers: Optional[Mapping[str, str]] = None,
34
+ ) -> JSONResponse:
35
+ tid = trace_id or _ensure_trace_id(request)
36
+
37
+ if isinstance(content, dict):
38
+ payload = content
39
+ else:
40
+ payload = content.model_dump(exclude_none=True) if hasattr(content, "model_dump") else content
41
+
42
+ payload["trace_id"] = tid
43
+
44
+ base_headers = {"X-Trace-Id": tid}
45
+ if headers:
46
+ base_headers.update(headers)
47
+
48
+ return JSONResponse(
49
+ status_code=status_code,
50
+ content=payload,
51
+ headers=base_headers
52
+ )
53
+
54
+
55
+ def install_exception_handlers(
56
+ app: FastAPI,
57
+ *,
58
+ logger: Any = None,
59
+ expose_server_errors: bool = False,
60
+ ) -> None:
61
+ async def _log_exception(
62
+ request: Request,
63
+ exc: Exception,
64
+ level: str = "warning",
65
+ extra: Optional[dict] = None
66
+ ):
67
+ if not logger:
68
+ return
69
+
70
+ log_payload = {
71
+ "path": request.url.path,
72
+ "method": request.method,
73
+ **extra
74
+ } if extra else {"path": request.url.path}
75
+
76
+ log_method = getattr(logger, level, logger.warning)
77
+ log_method(str(exc), extra=log_payload)
78
+
79
+ @app.exception_handler(BaseHttpException)
80
+ async def _handle_base_http_exception(request: Request, exc: BaseHttpException):
81
+ await _log_exception(request, exc, extra={"status": exc.status_code, "detail": exc.message})
82
+
83
+ locale = getattr(request.state, "locale", None)
84
+
85
+ main_message = _(exc.message, locale=locale)
86
+
87
+ translated_errors = []
88
+ if exc.errors:
89
+ for err in exc.errors:
90
+ err_copy = err.model_copy()
91
+ if err_copy.message:
92
+ err_copy.message = _(err_copy.message, locale=locale)
93
+ translated_errors.append(err_copy)
94
+
95
+ response_model = ErrorResponse(
96
+ message=main_message,
97
+ errors=translated_errors
98
+ )
99
+
100
+ return _json_response(request, exc.status_code, content=response_model)
101
+
102
+ @app.exception_handler(RequestValidationError)
103
+ async def _handle_request_validation_error(request: Request, exc: RequestValidationError):
104
+ await _log_exception(request, exc, level="info", extra={"errors": exc.errors()})
105
+
106
+ normalized_exception = UnprocessableEntityException(
107
+ detail="Validation Error",
108
+ validation_errors=exc.errors()
109
+ )
110
+ return await _handle_base_http_exception(request, normalized_exception)
111
+
112
+ @app.exception_handler(ValidationError)
113
+ async def _handle_pydantic_validation_error(request: Request, exc: ValidationError):
114
+ await _log_exception(request, exc, level="info", extra={"errors": exc.errors()})
115
+
116
+ normalized_exception = UnprocessableEntityException(
117
+ detail="Validation Error",
118
+ validation_errors=exc.errors()
119
+ )
120
+ return await _handle_base_http_exception(request, normalized_exception)
121
+
122
+ @app.exception_handler(StarletteHTTPException)
123
+ async def _handle_starlette_http_exception(request: Request, exc: StarletteHTTPException):
124
+ await _log_exception(request, exc, extra={"status": exc.status_code})
125
+
126
+ locale = getattr(request.state, "locale", None)
127
+ message_str = str(exc.detail) if exc.detail else "HTTP Error"
128
+ translated_message = _(message_str, locale=locale)
129
+
130
+ error_detail = ErrorDetail(
131
+ code=exc.status_code,
132
+ message=translated_message
133
+ )
134
+
135
+ response_model = ErrorResponse(
136
+ message=translated_message,
137
+ errors=[error_detail]
138
+ )
139
+
140
+ return _json_response(request, exc.status_code, content=response_model)
141
+
142
+ @app.exception_handler(Exception)
143
+ async def _handle_unexpected_exception(request: Request, exc: Exception):
144
+ tid = _ensure_trace_id(request)
145
+ await _log_exception(request, exc, level="exception", extra={"trace_id": tid})
146
+
147
+ raw_detail = str(exc) if expose_server_errors else "Internal Server Error"
148
+ locale = getattr(request.state, "locale", None)
149
+ translated_detail = _(raw_detail, locale=locale)
150
+
151
+ error_detail = ErrorDetail(
152
+ code=HTTP_500_INTERNAL_SERVER_ERROR,
153
+ message=translated_detail
154
+ )
155
+
156
+ response_model = ErrorResponse(
157
+ message=translated_detail,
158
+ errors=[error_detail]
159
+ )
160
+
161
+ return _json_response(
162
+ request,
163
+ HTTP_500_INTERNAL_SERVER_ERROR,
164
+ content=response_model,
165
+ trace_id=tid
166
+ )
@@ -0,0 +1,153 @@
1
+ from http import HTTPStatus
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from fastapi import HTTPException
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ErrorDetail(BaseModel):
9
+ code: int | str = 0
10
+ message: Optional[str] = None
11
+ fields: Optional[Union[List[Union[str, int]], str]] = None
12
+ data: Optional[Union[List[Dict], Dict]] = None
13
+
14
+
15
+ class BaseHttpException(HTTPException):
16
+ def __init__(
17
+ self,
18
+ status_code: int,
19
+ detail: Optional[str] = None,
20
+ errors: Optional[List[ErrorDetail]] = None,
21
+ fields: Optional[Union[List[Union[str, int]], str]] = None,
22
+ data: Optional[Dict] = None,
23
+ code: Optional[str] = None,
24
+ ):
25
+ if errors is None:
26
+ message = detail or HTTPStatus(status_code).phrase
27
+
28
+ if not code:
29
+ code = HTTPStatus(status_code).phrase.lower().replace(" ", "_")
30
+
31
+ errors = [
32
+ ErrorDetail(
33
+ code=code,
34
+ message=message,
35
+ fields=fields,
36
+ data=data
37
+ )
38
+ ]
39
+
40
+ self.errors = errors
41
+ super().__init__(status_code=status_code, detail=self.message)
42
+
43
+ @property
44
+ def message(self) -> str:
45
+ if self.errors and self.errors[0].message:
46
+ return self.errors[0].message
47
+ return HTTPStatus(self.status_code).phrase
48
+
49
+
50
+ class UnprocessableEntityException(BaseHttpException):
51
+ def __init__(
52
+ self,
53
+ detail: str = None,
54
+ fields: Optional[Union[List[Union[str, int]], str]] = None,
55
+ validation_errors: Optional[List[Dict[str, Any]]] = None
56
+ ):
57
+ errors = None
58
+
59
+ if validation_errors:
60
+ errors = []
61
+ for error in validation_errors:
62
+ err_type = error.get("type")
63
+ loc = error.get("loc")
64
+ msg = error.get("msg")
65
+ inp = error.get("input")
66
+
67
+ if err_type == "json_invalid":
68
+ loc = ["body"]
69
+ msg = "Invalid JSON format. Please check your syntax (e.g., convert '۱' to '1')."
70
+
71
+ elif isinstance(loc, tuple):
72
+ loc = list(loc)
73
+
74
+ error_data = {}
75
+ if inp is not None and not (isinstance(inp, dict) and not inp):
76
+ error_data["input"] = inp
77
+
78
+ errors.append(
79
+ ErrorDetail(
80
+ code=err_type or "validation_error",
81
+ message=msg,
82
+ fields=loc,
83
+ data=error_data if error_data else None
84
+ )
85
+ )
86
+
87
+ super().__init__(
88
+ status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
89
+ detail=detail,
90
+ fields=fields,
91
+ errors=errors
92
+ )
93
+
94
+
95
+ class BadRequestException(BaseHttpException):
96
+ def __init__(self, detail: str = None, code: int = 0):
97
+ super().__init__(status_code=HTTPStatus.BAD_REQUEST, detail=detail, code=code)
98
+
99
+
100
+ class UnauthorizedException(BaseHttpException):
101
+ def __init__(self, detail: str = None):
102
+ super().__init__(status_code=HTTPStatus.UNAUTHORIZED, detail=detail)
103
+
104
+
105
+ class ForbiddenException(BaseHttpException):
106
+ def __init__(self, detail: str = None):
107
+ super().__init__(status_code=HTTPStatus.FORBIDDEN, detail=detail)
108
+
109
+
110
+ class NotFoundException(BaseHttpException):
111
+ def __init__(self, detail: str = None):
112
+ super().__init__(status_code=HTTPStatus.NOT_FOUND, detail=detail)
113
+
114
+
115
+ class ConflictException(BaseHttpException):
116
+ def __init__(self, detail: str = None, data: Optional[Union[List[Dict], Dict]] = None):
117
+ super().__init__(status_code=HTTPStatus.CONFLICT, detail=detail, data=data)
118
+
119
+
120
+ class NotSupportedException(BaseHttpException):
121
+ def __init__(self, detail: str = None):
122
+ super().__init__(status_code=HTTPStatus.NOT_IMPLEMENTED, detail=detail)
123
+
124
+
125
+ class InternalServerException(BaseHttpException):
126
+ def __init__(self, detail: str = None):
127
+ super().__init__(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=detail)
128
+
129
+
130
+ class LogicalValidationException(UnprocessableEntityException):
131
+ def __init__(self, detail: str = None,
132
+ fields: Optional[Union[List[str], str]] = None,
133
+ validation_errors: Optional[List[Dict[str, Any]]] = None):
134
+ super().__init__(
135
+ detail=detail,
136
+ fields=fields,
137
+ validation_errors=validation_errors
138
+ )
139
+
140
+
141
+ class GoneException(BaseHttpException):
142
+ def __init__(self, detail: str = None):
143
+ super().__init__(status_code=HTTPStatus.GONE, detail=detail)
144
+
145
+
146
+ class TooManyRequestsException(BaseHttpException):
147
+ def __init__(self, detail: str = None):
148
+ super().__init__(status_code=HTTPStatus.TOO_MANY_REQUESTS, detail=detail)
149
+
150
+
151
+ class NotAcceptableException(BaseHttpException):
152
+ def __init__(self, detail: str = None):
153
+ super().__init__(status_code=HTTPStatus.NOT_ACCEPTABLE, detail=detail)
@@ -3,3 +3,4 @@ from nlbone.interfaces.api.schema import BaseResponseModel
3
3
 
4
4
  class FileOut(BaseResponseModel):
5
5
  url: str
6
+ id: int | str = None
@@ -0,0 +1,9 @@
1
+ def flatten_dict(d, parent_key='', sep='.'):
2
+ items = {}
3
+ for k, v in d.items():
4
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
5
+ if isinstance(v, dict):
6
+ items.update(flatten_dict(v, new_key, sep))
7
+ else:
8
+ items[new_key] = v
9
+ return items
@@ -1,35 +0,0 @@
1
- from typing import Dict, Optional
2
-
3
- from nlbone.core.ports.translation import TranslationPort
4
- from nlbone.utils.context import get_locale
5
-
6
- from .loaders import BaseLoader
7
-
8
-
9
- class I18nAdapter(TranslationPort):
10
- def __init__(self, loader: BaseLoader, default_locale: str = "fa-IR"):
11
- self.default_locale = default_locale
12
- self.loader = loader
13
- self._translations: Optional[Dict[str, Dict[str, str]]] = None
14
-
15
- def _ensure_loaded(self):
16
- if self._translations is None:
17
- self._translations = self.loader.load()
18
-
19
- def translate(self, key: str, locale: Optional[str] = None, **kwargs) -> str:
20
- self._ensure_loaded()
21
-
22
- target_locale = locale or self.default_locale
23
-
24
- locale_data = self._translations.get(target_locale, {})
25
- text = locale_data.get(key)
26
-
27
- if text is None:
28
- text = self._translations.get(self.default_locale, {}).get(key, key)
29
-
30
- if kwargs:
31
- try:
32
- return text.format(**kwargs)
33
- except KeyError:
34
- pass
35
- return text
@@ -1,10 +0,0 @@
1
- {
2
- "auth": {
3
- "otp_sent": "کد تایید برای {mobile} ارسال شد."
4
- },
5
- "order": {
6
- "remove": {
7
- "forbidden": "شما مجاز به حذف این سفارش نیستید"
8
- }
9
- }
10
- }
@@ -1,114 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any, Mapping, Optional
4
- from uuid import uuid4
5
-
6
- from fastapi import FastAPI, Request
7
- from fastapi import HTTPException as FastAPIHTTPException
8
- from fastapi.exceptions import RequestValidationError
9
- from fastapi.responses import JSONResponse
10
- from pydantic import ValidationError
11
- from starlette.exceptions import HTTPException as StarletteHTTPException
12
-
13
- from .exceptions import BaseHttpException
14
-
15
- from nlbone.adapters.i18n import translator as _
16
- # ---- Helpers ---------------------------------------------------------------
17
-
18
-
19
- def _ensure_trace_id(request: Request) -> str:
20
- rid = request.headers.get("X-Request-Id") or request.headers.get("X-Trace-Id")
21
- return rid or str(uuid4())
22
-
23
-
24
- def _json_response(
25
- request: Request,
26
- status_code: int,
27
- *,
28
- detail: Any,
29
- headers: Optional[Mapping[str, str]] = None,
30
- trace_id: Optional[str] = None,
31
- extra: Optional[Mapping[str, Any]] = None,
32
- ) -> JSONResponse:
33
- payload: dict[str, Any] = {"detail": detail}
34
- if extra:
35
- payload.update(extra)
36
- tid = trace_id or _ensure_trace_id(request)
37
-
38
- payload.setdefault("trace_id", tid)
39
-
40
- base_headers = {"X-Trace-Id": tid}
41
- if headers:
42
- base_headers.update(headers)
43
-
44
- return JSONResponse(status_code=status_code, content=payload, headers=base_headers)
45
-
46
-
47
- # ---- Public Installer ------------------------------------------------------
48
-
49
-
50
- def install_exception_handlers(
51
- app: FastAPI,
52
- *,
53
- logger: Any = None,
54
- expose_server_errors: bool = False,
55
- ) -> None:
56
- @app.exception_handler(BaseHttpException)
57
- async def _handle_base_http_exception(request: Request, exc: BaseHttpException):
58
- if logger:
59
- logger.warning(
60
- "http_error",
61
- extra={"status": exc.status_code, "detail": exc.detail, "path": request.url.path},
62
- )
63
- locale = locale = getattr(request.state, "locale", None)
64
- return _json_response(request, exc.status_code, detail=_(exc.detail, locale=locale))
65
-
66
- @app.exception_handler(FastAPIHTTPException)
67
- async def _handle_fastapi_http_exception(request: Request, exc: FastAPIHTTPException):
68
- if logger:
69
- logger.warning(
70
- "fastapi_http_error",
71
- extra={"status": exc.status_code, "detail": exc.detail, "path": request.url.path},
72
- )
73
- locale = locale = getattr(request.state, "locale", None)
74
- return _json_response(request, exc.status_code, detail=_(exc.detail, locale=locale))
75
-
76
- @app.exception_handler(StarletteHTTPException)
77
- async def _handle_starlette_http_exception(request: Request, exc: StarletteHTTPException):
78
- if logger:
79
- logger.warning(
80
- "starlette_http_error",
81
- extra={"status": exc.status_code, "detail": exc.detail, "path": request.url.path},
82
- )
83
- locale = locale = getattr(request.state, "locale", None)
84
- return _json_response(request, exc.status_code, detail=_(exc.detail, locale=locale))
85
-
86
- # 3) خطاهای اعتبارسنجی FastAPI (request body/query/path)
87
- @app.exception_handler(RequestValidationError)
88
- async def _handle_request_validation_error(request: Request, exc: RequestValidationError):
89
- errors = exc.errors()
90
- if logger:
91
- logger.info(
92
- "request_validation_error",
93
- extra={"errors": errors, "path": request.url.path},
94
- )
95
- return _json_response(request, 422, detail=errors)
96
-
97
- @app.exception_handler(ValidationError)
98
- async def _handle_pydantic_validation_error(request: Request, exc: ValidationError):
99
- errors = exc.errors()
100
- if logger:
101
- logger.info(
102
- "pydantic_validation_error",
103
- extra={"errors": errors, "path": request.url.path},
104
- )
105
- return _json_response(request, 422, detail=errors)
106
-
107
- @app.exception_handler(Exception)
108
- async def _handle_unexpected_exception(request: Request, exc: Exception):
109
- tid = _ensure_trace_id(request)
110
- if logger:
111
- logger.exception("unhandled_exception", extra={"trace_id": tid, "path": request.url.path})
112
- detail = str(exc) if expose_server_errors else "internal server error"
113
- locale = locale = getattr(request.state, "locale", None)
114
- return _json_response(request, 500, detail=_(detail, locale=locale), trace_id=tid)
@@ -1,108 +0,0 @@
1
- from typing import Any, Iterable, Dict, List
2
-
3
- from fastapi import HTTPException, status
4
-
5
-
6
- def _error_entry(loc: Iterable[Any] | None, detail: str, type_: str) -> dict:
7
- return {
8
- "loc": list(loc) if loc else [],
9
- "msg": detail,
10
- "type": type_,
11
- }
12
-
13
-
14
- def _errors(loc: Iterable[Any] | None, detail: str, type_: str) -> list[dict]:
15
- return [_error_entry(loc, detail, type_)]
16
-
17
-
18
- class BaseHttpException(HTTPException):
19
- pass
20
-
21
-
22
- class BadRequestException(BaseHttpException):
23
- def __init__(self, detail: str):
24
- super().__init__(
25
- status_code=status.HTTP_400_BAD_REQUEST,
26
- detail=detail,
27
- )
28
-
29
-
30
- class UnauthorizedException(BaseHttpException):
31
- def __init__(self, detail: str = "unauthorized"):
32
- super().__init__(
33
- status_code=status.HTTP_401_UNAUTHORIZED,
34
- detail=detail,
35
- )
36
-
37
-
38
- class ForbiddenException(BaseHttpException):
39
- def __init__(self, detail: str = "forbidden"):
40
- super().__init__(
41
- status_code=status.HTTP_403_FORBIDDEN,
42
- detail=detail,
43
- )
44
-
45
-
46
- class NotFoundException(BaseHttpException):
47
- def __init__(self, detail: str = "not found"):
48
- super().__init__(
49
- status_code=status.HTTP_404_NOT_FOUND,
50
- detail=detail,
51
- )
52
-
53
-
54
- class ConflictException(BaseHttpException):
55
- def __init__(self, detail: str = "conflict"):
56
- super().__init__(
57
- status_code=status.HTTP_409_CONFLICT,
58
- detail=detail,
59
- )
60
-
61
-
62
- class NotSupportedException(BaseHttpException):
63
- def __init__(self, detail: str = "NotSupported"):
64
- super().__init__(
65
- status_code=status.HTTP_501_NOT_IMPLEMENTED,
66
- detail=detail,
67
- )
68
-
69
-
70
- class InternalServerException(BaseHttpException):
71
- def __init__(self, detail: str = "internal_server_error"):
72
- super().__init__(
73
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
74
- detail=detail,
75
- )
76
-
77
-
78
- class UnprocessableEntityException(BaseHttpException):
79
- def __init__(self, detail: str, loc: Iterable[Any] | None = None, type_: str = "unprocessable_entity"):
80
- super().__init__(
81
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
82
- detail=_errors(loc, detail, type_),
83
- )
84
-
85
-
86
- class LogicalValidationException(HTTPException):
87
- def __init__(self, detail: str = None, loc: Iterable[Any] | None = None, type_: str = "logical_error",
88
- errors: List[Dict[str, Any]] = None):
89
- self.errors = errors
90
- if errors:
91
- super().__init__(detail=errors, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,)
92
- else:
93
- super().__init__(detail=_errors(loc, detail, type_), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
94
-
95
-
96
- class GoneException(BaseHttpException):
97
- def __init__(self, detail: str = "Gone"):
98
- super().__init__(
99
- status_code=status.HTTP_410_GONE,
100
- detail=detail,
101
- )
102
-
103
- class TooManyRequestsException(BaseHttpException):
104
- def __init__(self, detail: str = "TooManyRequests"):
105
- super().__init__(
106
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
107
- detail=detail,
108
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes