qena-shared-lib 0.1.12__tar.gz → 0.1.14__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 (50) hide show
  1. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/.pre-commit-config.yaml +8 -0
  2. qena_shared_lib-0.1.14/CHANGELOG.md +35 -0
  3. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/PKG-INFO +23 -20
  4. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/README.md +22 -19
  5. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/pyproject.toml +5 -10
  6. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/__init__.py +2 -2
  7. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/application.py +71 -29
  8. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/background.py +6 -5
  9. qena_shared_lib-0.1.14/src/qena_shared_lib/exception_handlers.py +235 -0
  10. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/exceptions.py +10 -10
  11. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/http.py +16 -16
  12. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/__init__.py +8 -6
  13. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/_base.py +96 -127
  14. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/_channel.py +4 -1
  15. qena_shared_lib-0.1.14/src/qena_shared_lib/rabbitmq/_exception_handlers.py +182 -0
  16. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/_listener.py +94 -38
  17. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/_rpc_client.py +4 -4
  18. qena_shared_lib-0.1.14/src/qena_shared_lib/remotelogging/__init__.py +15 -0
  19. {qena_shared_lib-0.1.12/src/qena_shared_lib/logstash → qena_shared_lib-0.1.14/src/qena_shared_lib/remotelogging}/_base.py +47 -67
  20. qena_shared_lib-0.1.14/src/qena_shared_lib/remotelogging/logstash/__init__.py +9 -0
  21. qena_shared_lib-0.1.14/src/qena_shared_lib/remotelogging/logstash/_base.py +32 -0
  22. {qena_shared_lib-0.1.12/src/qena_shared_lib → qena_shared_lib-0.1.14/src/qena_shared_lib/remotelogging}/logstash/_http_sender.py +5 -4
  23. {qena_shared_lib-0.1.12/src/qena_shared_lib → qena_shared_lib-0.1.14/src/qena_shared_lib/remotelogging}/logstash/_tcp_sender.py +7 -5
  24. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/scheduler.py +49 -24
  25. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/security.py +2 -2
  26. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/utils.py +9 -3
  27. qena_shared_lib-0.1.14/tests/conftest.py +25 -0
  28. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_application.py +49 -48
  29. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_background.py +13 -13
  30. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_logstash.py +94 -79
  31. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_rabbitmq.py +349 -320
  32. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_scheduler.py +16 -13
  33. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_security.py +39 -28
  34. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/uv.lock +801 -800
  35. qena_shared_lib-0.1.12/CHANGELOG.md +0 -14
  36. qena_shared_lib-0.1.12/requirements.txt +0 -55
  37. qena_shared_lib-0.1.12/src/qena_shared_lib/exception_handlers.py +0 -207
  38. qena_shared_lib-0.1.12/src/qena_shared_lib/logstash/__init__.py +0 -17
  39. qena_shared_lib-0.1.12/src/qena_shared_lib/rabbitmq/_exception_handlers.py +0 -147
  40. qena_shared_lib-0.1.12/tests/conftest.py +0 -25
  41. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/.gitignore +0 -0
  42. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/dependencies/__init__.py +0 -0
  43. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/dependencies/http.py +0 -0
  44. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/dependencies/miscellaneous.py +0 -0
  45. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/logging.py +0 -0
  46. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/py.typed +0 -0
  47. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/_pool.py +0 -0
  48. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/src/qena_shared_lib/rabbitmq/_publisher.py +0 -0
  49. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/test_dependencies.py +0 -0
  50. {qena_shared_lib-0.1.12 → qena_shared_lib-0.1.14}/tests/utils.py +0 -0
@@ -17,3 +17,11 @@ repos:
17
17
  rev: v1.15.0
18
18
  hooks:
19
19
  - id: mypy
20
+ - repo: local
21
+ hooks:
22
+ - id: pytest
23
+ name: pytest
24
+ entry: bash -c 'PYTHONPATH=$(pwd) pytest tests'
25
+ language: system
26
+ types: [python]
27
+ pass_filenames: false
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## [0.1.14] - 2025-06-16
4
+
5
+ ### Changed
6
+
7
+ - Remove unused generics variables
8
+ - Remove requirements.txt
9
+
10
+
11
+ ## [0.1.13] - 2025-06-16
12
+
13
+ ### Added
14
+
15
+ - Added pytest as one of pre-commit steps.
16
+ - Made http and rabbitmq exception handlers class based.
17
+ - Gracefull shutdown for rabbitmq and scheduler.
18
+
19
+ ### Changed
20
+
21
+ - Moved logstash to remotelogging to generalize other forms of logging.
22
+ - Added loop class attribute for async event loop mixin.
23
+ - Made mypy type check slightly more strict.
24
+
25
+
26
+ ## [0.1.12] - 2025-04-05
27
+
28
+ ### Added
29
+
30
+ - Added a re-export for rabbitmq channel pool (ChannelPool) class.
31
+
32
+
33
+ [0.1.14]: https://github.com/Qena-Digital-Lending/qena-shared-kernel/compare/v0.1.13...v0.1.14
34
+ [0.1.13]: https://github.com/Qena-Digital-Lending/qena-shared-kernel/compare/v0.1.12...v0.1.13
35
+ [0.1.12]: https://github.com/Qena-Digital-Lending/qena-shared-kernel/compare/v0.1.11...v0.1.12
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qena-shared-lib
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: A shared tools for other services
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: cronsim==2.6
@@ -21,7 +21,8 @@ A shared tools for other services. It includes.
21
21
  - FastAPI app builder
22
22
  - A wrapper around fastapi to make it class based.
23
23
  - RabbitMQ utility class to listen, respond, publish and make rpc request.
24
- - Logstash utility class to log message in `ecs` ( elastic common schema ).
24
+ - Remote logging
25
+ - Logstash utility class to log message in `ecs` ( elastic common schema ).
25
26
  - A simple task scheduler, to schedule task to run in specific time.
26
27
  - Background task runner.
27
28
  - Security tools ( password hasher, jwt, acl ).
@@ -178,41 +179,43 @@ def main() -> FastAPI:
178
179
  ...
179
180
  ```
180
181
 
181
- ## Logstash
182
+ ## Remote logging
182
183
 
184
+ ### Logstash
183
185
  ``` py
184
- from qena_shared_lib.logstash import BaseLogstashSender, HTTPSender, # TCPSender
186
+ from qena_shared_lib.remotelogging import BaseRemoteLogSender
187
+ from qena_shared_lib.remotelogging.logstash import HTTPSender, # TCPSender
185
188
 
186
189
 
187
190
  @asynccontextmanager
188
191
  async def lifespan(app: FastAPI):
189
- logstash = get_service(BaseLogstashSender)
192
+ remote_logger = get_service(BaseRemoteLogSender)
190
193
 
191
- await logstash.start()
194
+ await remote_logger.start()
192
195
 
193
196
  yield
194
197
 
195
- await logstash.stop()
198
+ await remote_logger.stop()
196
199
 
197
200
 
198
201
  def main() -> FastAPI:
199
202
  ...
200
203
 
201
- logstash = HTTPSender(
204
+ remote_logger = HTTPSender(
202
205
  service_name="qena-shared-lib",
203
206
  url="http://127.0.0.1:18080",
204
207
  user="logstash",
205
208
  password="logstash",
206
209
  )
207
210
  # or
208
- # logstash = TCPSender(
211
+ # remote_logger = TCPSender(
209
212
  # service_name="qena-shared-lib",
210
213
  # host="127.0.0.1",
211
214
  # port=18090
212
215
  # )
213
216
  builder.with_singleton(
214
- service=BaseLogstashSender,
215
- instance=logstash,
217
+ service=BaseRemoteLogSender,
218
+ instance=remote_logger,
216
219
  )
217
220
 
218
221
  ...
@@ -220,13 +223,13 @@ def main() -> FastAPI:
220
223
 
221
224
  @router.get("")
222
225
  def log_message(
223
- logstash: Annotated[
224
- BaseLogstashSender,
225
- DependsOn(BaseLogstashSender),
226
+ remote_logger: Annotated[
227
+ BaseRemoteLogSender,
228
+ DependsOn(BaseRemoteLogSender),
226
229
  ],
227
230
  message: str,
228
231
  ):
229
- logstash.info(message)
232
+ remote_logger.info(message)
230
233
  ```
231
234
 
232
235
  ## Rabbitmq
@@ -263,7 +266,7 @@ def main() -> FastAPI:
263
266
  ...
264
267
 
265
268
  rabbitmq = RabbitMqManager(
266
- logstash=logstash,
269
+ remote_logger=remote_logger,
267
270
  container=builder.container,
268
271
  )
269
272
 
@@ -402,7 +405,7 @@ def main() -> FastAPI:
402
405
  ...
403
406
 
404
407
  rabbitmq = RabbitMqManager(
405
- logstash=logstash,
408
+ remote_logger=remote_logger,
406
409
  container=builder.container,
407
410
  # or globally for all consumers
408
411
  listener_global_retry_policy=RetryPolicy(
@@ -473,7 +476,7 @@ def main() -> FastAPI:
473
476
  ...
474
477
 
475
478
  schedule_manager = ScheduleManager(
476
- logstash=logstash,
479
+ remote_logger=remote_logger,
477
480
  container=builder.container
478
481
  )
479
482
 
@@ -507,8 +510,8 @@ def main() -> FastAPI:
507
510
  ...
508
511
 
509
512
  builder.with_singleton(
510
- service=BaseLogstashSender,
511
- instance=logstash,
513
+ service=BaseRemoteLogSender,
514
+ instance=remote_logger,
512
515
  )
513
516
  builder.with_singleton(Background)
514
517
 
@@ -5,7 +5,8 @@ A shared tools for other services. It includes.
5
5
  - FastAPI app builder
6
6
  - A wrapper around fastapi to make it class based.
7
7
  - RabbitMQ utility class to listen, respond, publish and make rpc request.
8
- - Logstash utility class to log message in `ecs` ( elastic common schema ).
8
+ - Remote logging
9
+ - Logstash utility class to log message in `ecs` ( elastic common schema ).
9
10
  - A simple task scheduler, to schedule task to run in specific time.
10
11
  - Background task runner.
11
12
  - Security tools ( password hasher, jwt, acl ).
@@ -162,41 +163,43 @@ def main() -> FastAPI:
162
163
  ...
163
164
  ```
164
165
 
165
- ## Logstash
166
+ ## Remote logging
166
167
 
168
+ ### Logstash
167
169
  ``` py
168
- from qena_shared_lib.logstash import BaseLogstashSender, HTTPSender, # TCPSender
170
+ from qena_shared_lib.remotelogging import BaseRemoteLogSender
171
+ from qena_shared_lib.remotelogging.logstash import HTTPSender, # TCPSender
169
172
 
170
173
 
171
174
  @asynccontextmanager
172
175
  async def lifespan(app: FastAPI):
173
- logstash = get_service(BaseLogstashSender)
176
+ remote_logger = get_service(BaseRemoteLogSender)
174
177
 
175
- await logstash.start()
178
+ await remote_logger.start()
176
179
 
177
180
  yield
178
181
 
179
- await logstash.stop()
182
+ await remote_logger.stop()
180
183
 
181
184
 
182
185
  def main() -> FastAPI:
183
186
  ...
184
187
 
185
- logstash = HTTPSender(
188
+ remote_logger = HTTPSender(
186
189
  service_name="qena-shared-lib",
187
190
  url="http://127.0.0.1:18080",
188
191
  user="logstash",
189
192
  password="logstash",
190
193
  )
191
194
  # or
192
- # logstash = TCPSender(
195
+ # remote_logger = TCPSender(
193
196
  # service_name="qena-shared-lib",
194
197
  # host="127.0.0.1",
195
198
  # port=18090
196
199
  # )
197
200
  builder.with_singleton(
198
- service=BaseLogstashSender,
199
- instance=logstash,
201
+ service=BaseRemoteLogSender,
202
+ instance=remote_logger,
200
203
  )
201
204
 
202
205
  ...
@@ -204,13 +207,13 @@ def main() -> FastAPI:
204
207
 
205
208
  @router.get("")
206
209
  def log_message(
207
- logstash: Annotated[
208
- BaseLogstashSender,
209
- DependsOn(BaseLogstashSender),
210
+ remote_logger: Annotated[
211
+ BaseRemoteLogSender,
212
+ DependsOn(BaseRemoteLogSender),
210
213
  ],
211
214
  message: str,
212
215
  ):
213
- logstash.info(message)
216
+ remote_logger.info(message)
214
217
  ```
215
218
 
216
219
  ## Rabbitmq
@@ -247,7 +250,7 @@ def main() -> FastAPI:
247
250
  ...
248
251
 
249
252
  rabbitmq = RabbitMqManager(
250
- logstash=logstash,
253
+ remote_logger=remote_logger,
251
254
  container=builder.container,
252
255
  )
253
256
 
@@ -386,7 +389,7 @@ def main() -> FastAPI:
386
389
  ...
387
390
 
388
391
  rabbitmq = RabbitMqManager(
389
- logstash=logstash,
392
+ remote_logger=remote_logger,
390
393
  container=builder.container,
391
394
  # or globally for all consumers
392
395
  listener_global_retry_policy=RetryPolicy(
@@ -457,7 +460,7 @@ def main() -> FastAPI:
457
460
  ...
458
461
 
459
462
  schedule_manager = ScheduleManager(
460
- logstash=logstash,
463
+ remote_logger=remote_logger,
461
464
  container=builder.container
462
465
  )
463
466
 
@@ -491,8 +494,8 @@ def main() -> FastAPI:
491
494
  ...
492
495
 
493
496
  builder.with_singleton(
494
- service=BaseLogstashSender,
495
- instance=logstash,
497
+ service=BaseRemoteLogSender,
498
+ instance=remote_logger,
496
499
  )
497
500
  builder.with_singleton(Background)
498
501
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qena-shared-lib"
3
- version = "0.1.12"
3
+ version = "0.1.14"
4
4
  description = "A shared tools for other services"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -74,13 +74,8 @@ skip-magic-trailing-comma = false
74
74
  line-ending = "auto"
75
75
 
76
76
  [tool.mypy]
77
+ strict = true
77
78
  explicit_package_bases = true
78
- ignore_missing_imports = true
79
- disallow_untyped_defs = true
80
- disallow_incomplete_defs = true
81
- check_untyped_defs = true
82
- warn_redundant_casts = true
83
- warn_unreachable = true
84
- allow_redefinition = true
85
- local_partial_types = true
86
- strict_equality = true
79
+ disallow_untyped_decorators = false
80
+ disallow_subclassing_any = false
81
+ warn_unused_ignores = false
@@ -5,8 +5,8 @@ from . import (
5
5
  exceptions,
6
6
  http,
7
7
  logging,
8
- logstash,
9
8
  rabbitmq,
9
+ remotelogging,
10
10
  scheduler,
11
11
  security,
12
12
  utils,
@@ -19,7 +19,7 @@ __all__ = [
19
19
  "exceptions",
20
20
  "http",
21
21
  "logging",
22
- "logstash",
22
+ "remotelogging",
23
23
  "rabbitmq",
24
24
  "scheduler",
25
25
  "security",
@@ -2,17 +2,17 @@ from enum import Enum
2
2
  from typing import Any, TypeVar
3
3
 
4
4
  from fastapi import APIRouter, FastAPI
5
- from fastapi.exceptions import RequestValidationError
6
5
  from prometheus_fastapi_instrumentator import Instrumentator
7
6
  from punq import Container, Scope, empty
8
7
  from starlette.types import Lifespan
8
+ from typing_extensions import Self
9
9
 
10
10
  from .exception_handlers import (
11
- handle_general_http_exception,
12
- handle_http_service_error,
13
- handle_request_validation_error,
11
+ AbstractHttpExceptionHandler,
12
+ GeneralHttpExceptionHandler,
13
+ HTTPServiceExceptionHandler,
14
+ RequestValidationErrorHandler,
14
15
  )
15
- from .exceptions import ServiceException
16
16
  from .http import ControllerBase
17
17
 
18
18
  __all__ = [
@@ -21,6 +21,7 @@ __all__ = [
21
21
  "FastAPI",
22
22
  ]
23
23
 
24
+
24
25
  D = TypeVar("D")
25
26
 
26
27
 
@@ -46,7 +47,7 @@ class Builder:
46
47
  self._container = Container()
47
48
  self._built = False
48
49
 
49
- def with_environment(self, environment: Environment) -> "Builder":
50
+ def with_environment(self, environment: Environment) -> Self:
50
51
  match environment:
51
52
  case Environment.DEVELOPMENT:
52
53
  self._environment = Environment.DEVELOPMENT
@@ -60,35 +61,33 @@ class Builder:
60
61
 
61
62
  return self
62
63
 
63
- def with_title(self, title: str) -> "Builder":
64
+ def with_title(self, title: str) -> Self:
64
65
  self._title = title
65
66
 
66
67
  return self
67
68
 
68
- def with_description(self, description: str) -> "Builder":
69
+ def with_description(self, description: str) -> Self:
69
70
  self._description = description
70
71
 
71
72
  return self
72
73
 
73
- def with_version(self, version: str) -> "Builder":
74
+ def with_version(self, version: str) -> Self:
74
75
  self._version = version
75
76
 
76
77
  return self
77
78
 
78
- def with_lifespan(self, lifespan: Lifespan) -> "Builder":
79
+ def with_lifespan(self, lifespan: Lifespan) -> Self:
79
80
  self._lifespan = lifespan
80
81
 
81
82
  return self
82
83
 
83
- def with_controllers(
84
- self, controllers: list[type[ControllerBase]]
85
- ) -> "Builder":
84
+ def with_controllers(self, *controllers: type[ControllerBase]) -> Self:
86
85
  for index, controller in enumerate(controllers):
87
86
  if not isinstance(controller, type) or not issubclass(
88
87
  controller, ControllerBase
89
88
  ):
90
89
  raise TypeError(
91
- f"controller {index} is {type(ControllerBase)}, expected instance of type or subclass of `ControllerBase`"
90
+ f"controller {index} is {type(controller)}, expected instance of type or subclass of `ControllerBase`"
92
91
  )
93
92
 
94
93
  self._container.register(
@@ -99,7 +98,7 @@ class Builder:
99
98
 
100
99
  return self
101
100
 
102
- def with_routers(self, routers: list[APIRouter]) -> "Builder":
101
+ def with_routers(self, *routers: APIRouter) -> Self:
103
102
  if any(not isinstance(router, APIRouter) for router in routers):
104
103
  raise TypeError("some routers are not type `APIRouter`")
105
104
 
@@ -107,13 +106,41 @@ class Builder:
107
106
 
108
107
  return self
109
108
 
109
+ def with_exception_handlers(
110
+ self, *exception_handlers: type[AbstractHttpExceptionHandler]
111
+ ) -> Self:
112
+ for index, exception_handler in enumerate(exception_handlers):
113
+ if not isinstance(exception_handler, type) or not issubclass(
114
+ exception_handler, AbstractHttpExceptionHandler
115
+ ):
116
+ raise TypeError(
117
+ f"exception handler {index} is {type(exception_handler)}, expected instance of type or subclass of `AbstractHttpExceptionHandler`"
118
+ )
119
+
120
+ self._container.register(
121
+ service=AbstractHttpExceptionHandler,
122
+ factory=exception_handler,
123
+ scope=Scope.singleton,
124
+ )
125
+
126
+ return self
127
+
128
+ def with_default_exception_handlers(self) -> Self:
129
+ self.with_exception_handlers(
130
+ GeneralHttpExceptionHandler,
131
+ HTTPServiceExceptionHandler,
132
+ RequestValidationErrorHandler,
133
+ )
134
+
135
+ return self
136
+
110
137
  def with_singleton(
111
138
  self,
112
139
  service: type[D],
113
140
  factory: Any = empty,
114
141
  instance: Any = empty,
115
142
  **kwargs: Any,
116
- ) -> "Builder":
143
+ ) -> Self:
117
144
  self._container.register(
118
145
  service=service,
119
146
  factory=factory,
@@ -126,7 +153,7 @@ class Builder:
126
153
 
127
154
  def with_transient(
128
155
  self, service: type[D], factory: Any = empty, **kwargs: Any
129
- ) -> "Builder":
156
+ ) -> Self:
130
157
  self._container.register(
131
158
  service=service,
132
159
  factory=factory,
@@ -136,7 +163,7 @@ class Builder:
136
163
 
137
164
  return self
138
165
 
139
- def with_metrics(self, endpoint: str = "/metrics") -> "Builder":
166
+ def with_metrics(self, endpoint: str = "/metrics") -> Self:
140
167
  self._metrics_endpoint = endpoint
141
168
  self._instrumentator = Instrumentator()
142
169
 
@@ -158,13 +185,8 @@ class Builder:
158
185
  )
159
186
  app.state.container = self._container
160
187
 
161
- app.exception_handler(ServiceException)(handle_http_service_error)
162
- app.exception_handler(RequestValidationError)(
163
- handle_request_validation_error
164
- )
165
- app.exception_handler(Exception)(handle_general_http_exception)
166
-
167
- self._resolve_api_controllers(app)
188
+ self._register_api_controllers(app)
189
+ self._register_exception_handlers(app)
168
190
 
169
191
  if self._instrumentator is not None:
170
192
  self._instrumentator.instrument(app).expose(
@@ -177,14 +199,34 @@ class Builder:
177
199
 
178
200
  return app
179
201
 
180
- def _resolve_api_controllers(self, app: FastAPI) -> None:
181
- api_controller_routers = [
202
+ def _register_api_controllers(self, app: FastAPI) -> None:
203
+ for router in self._routers + self._resolve_api_controllers():
204
+ app.include_router(router)
205
+
206
+ def _resolve_api_controllers(self) -> list[APIRouter]:
207
+ return [
182
208
  api_controller.register_route_handlers()
183
209
  for api_controller in self._container.resolve_all(ControllerBase)
184
210
  ]
185
211
 
186
- for router in self._routers + api_controller_routers:
187
- app.include_router(router)
212
+ def _register_exception_handlers(self, app: FastAPI) -> None:
213
+ for exception_handler in self._resolve_exception_handlers():
214
+ if not callable(exception_handler):
215
+ raise ValueError(
216
+ f"exception handler {exception_handler.__class__.__name__} is not callable"
217
+ )
218
+
219
+ app.exception_handler(exception_handler.exception)(
220
+ exception_handler
221
+ )
222
+
223
+ def _resolve_exception_handlers(self) -> list[AbstractHttpExceptionHandler]:
224
+ return [
225
+ exception_handler
226
+ for exception_handler in self._container.resolve_all(
227
+ AbstractHttpExceptionHandler
228
+ )
229
+ ]
188
230
 
189
231
  @property
190
232
  def environment(self) -> Environment:
@@ -3,13 +3,14 @@ from asyncio import (
3
3
  Task,
4
4
  gather,
5
5
  )
6
+ from typing import Any
6
7
  from uuid import uuid4
7
8
 
8
9
  from prometheus_client import Enum as PrometheusEnum
9
10
  from starlette.background import BackgroundTask
10
11
 
11
12
  from .logging import LoggerProvider
12
- from .logstash import BaseLogstashSender
13
+ from .remotelogging import BaseRemoteLogSender
13
14
  from .utils import AsyncEventLoopMixin
14
15
 
15
16
  __all__ = [
@@ -27,14 +28,14 @@ class Background(AsyncEventLoopMixin):
27
28
 
28
29
  def __init__(
29
30
  self,
30
- logstash: BaseLogstashSender,
31
+ remote_logger: BaseRemoteLogSender,
31
32
  ) -> None:
32
33
  self._queue: Queue[tuple[BackgroundTask | None, str | None]] = Queue()
33
34
  self._started = False
34
35
  self._stopped = False
35
- self._logstash = logstash
36
+ self._remote_logger = remote_logger
36
37
  self._logger = LoggerProvider.default().get_logger("backgroud")
37
- self._tasks: dict[str, Task] = {}
38
+ self._tasks: dict[str, Task[Any]] = {}
38
39
 
39
40
  async def _task_manager(
40
41
  self, task: BackgroundTask, task_id: str | None = None
@@ -53,7 +54,7 @@ class Background(AsyncEventLoopMixin):
53
54
 
54
55
  await self._tasks[task_id]
55
56
  except Exception:
56
- self._logstash.error(
57
+ self._remote_logger.error(
57
58
  "exception occured when running background task {task.func.__name__} with id {task_id}"
58
59
  )
59
60
  finally: