fastapi-factory-utilities 0.4.0__py3-none-any.whl → 0.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fastapi-factory-utilities might be problematic. Click here for more details.

Files changed (72) hide show
  1. fastapi_factory_utilities/core/api/__init__.py +1 -1
  2. fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
  3. fastapi_factory_utilities/core/app/__init__.py +4 -4
  4. fastapi_factory_utilities/core/app/application.py +22 -26
  5. fastapi_factory_utilities/core/app/builder.py +8 -32
  6. fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
  7. fastapi_factory_utilities/core/exceptions.py +64 -29
  8. fastapi_factory_utilities/core/plugins/__init__.py +2 -31
  9. fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
  10. fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
  11. fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
  12. fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
  13. fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
  14. fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
  15. fastapi_factory_utilities/core/plugins/aiopika/exchange.py +69 -0
  16. fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
  17. fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
  18. fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
  19. fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
  20. fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
  21. fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
  22. fastapi_factory_utilities/core/plugins/aiopika/queue.py +88 -0
  23. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +14 -157
  24. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +3 -3
  25. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
  26. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +1 -1
  27. fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
  28. fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
  29. fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +1 -0
  30. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -121
  31. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
  32. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
  33. fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +31 -0
  34. fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
  35. fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
  36. fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
  37. fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
  38. fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
  39. fastapi_factory_utilities/core/protocols.py +1 -54
  40. fastapi_factory_utilities/core/security/__init__.py +5 -0
  41. fastapi_factory_utilities/core/security/abstracts.py +42 -0
  42. fastapi_factory_utilities/core/security/jwt/__init__.py +45 -0
  43. fastapi_factory_utilities/core/security/jwt/configs.py +32 -0
  44. fastapi_factory_utilities/core/security/jwt/decoders.py +130 -0
  45. fastapi_factory_utilities/core/security/jwt/exceptions.py +23 -0
  46. fastapi_factory_utilities/core/security/jwt/objects.py +107 -0
  47. fastapi_factory_utilities/core/security/jwt/services.py +176 -0
  48. fastapi_factory_utilities/core/security/jwt/stores.py +43 -0
  49. fastapi_factory_utilities/core/security/jwt/types.py +9 -0
  50. fastapi_factory_utilities/core/security/jwt/verifiers.py +46 -0
  51. fastapi_factory_utilities/core/security/kratos.py +43 -43
  52. fastapi_factory_utilities/core/services/hydra/__init__.py +10 -3
  53. fastapi_factory_utilities/core/services/hydra/services.py +112 -34
  54. fastapi_factory_utilities/core/services/status/__init__.py +2 -2
  55. fastapi_factory_utilities/core/services/status/exceptions.py +1 -1
  56. fastapi_factory_utilities/core/utils/status.py +2 -1
  57. fastapi_factory_utilities/core/utils/yaml_reader.py +1 -1
  58. fastapi_factory_utilities/example/app.py +15 -5
  59. fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
  60. fastapi_factory_utilities/example/models/books/__init__.py +1 -1
  61. {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info}/METADATA +14 -8
  62. fastapi_factory_utilities-0.8.3.dist-info/RECORD +111 -0
  63. {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info}/WHEEL +1 -1
  64. fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
  65. fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
  66. fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
  67. fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
  68. fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
  69. fastapi_factory_utilities/core/security/jwt.py +0 -158
  70. fastapi_factory_utilities-0.4.0.dist-info/RECORD +0 -82
  71. {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info}/entry_points.txt +0 -0
  72. {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info/licenses}/LICENSE +0 -0
@@ -215,7 +215,7 @@ class ODMBuilder:
215
215
 
216
216
  if self._odm_client is None:
217
217
  raise ODMPluginConfigError(
218
- "ODM client is not set. Provide the ODM client using " "build_client method or through parameter."
218
+ "ODM client is not set. Provide the ODM client using build_client method or through parameter."
219
219
  )
220
220
 
221
221
  self._odm_database = self._odm_client.get_database(
@@ -242,7 +242,7 @@ class ODMBuilder:
242
242
 
243
243
  return self
244
244
 
245
- async def wait_ping(self):
245
+ async def wait_ping(self) -> None:
246
246
  """Wait for the ODM client to be ready.
247
247
 
248
248
  Returns:
@@ -250,7 +250,7 @@ class ODMBuilder:
250
250
  """
251
251
  if self._odm_client is None:
252
252
  raise ODMPluginConfigError(
253
- "ODM client is not set. Provide the ODM client using " "build_client method or through parameter."
253
+ "ODM client is not set. Provide the ODM client using build_client method or through parameter."
254
254
  )
255
255
 
256
256
  try:
@@ -12,4 +12,4 @@ class ODMConfig(BaseModel):
12
12
 
13
13
  database: str = "test"
14
14
 
15
- connection_timeout_ms: int = 1000
15
+ connection_timeout_ms: int = 4000
@@ -13,7 +13,7 @@ class BaseDocument(Document):
13
13
  """Base document class."""
14
14
 
15
15
  # To be agnostic of MongoDN, we use UUID as the document ID.
16
- id: UUID = Field( # pyright: ignore[reportIncompatibleVariableOverride]
16
+ id: UUID = Field( # type: ignore
17
17
  default_factory=uuid4, description="The document ID."
18
18
  )
19
19
 
@@ -0,0 +1,16 @@
1
+ """Helper functions for ODM plugins."""
2
+
3
+ import datetime
4
+ import uuid
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class PersistedEntity(BaseModel):
10
+ """Base class for persisted entities."""
11
+
12
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
13
+
14
+ revision_id: uuid.UUID | None = Field(default=None)
15
+ created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
16
+ updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
@@ -0,0 +1,155 @@
1
+ """Oriented Data Model (ODM) plugin package."""
2
+
3
+ from logging import INFO, Logger, getLogger
4
+ from typing import Any, Self
5
+
6
+ from beanie import Document, init_beanie # pyright: ignore[reportUnknownVariableType]
7
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
8
+ from reactivex import Subject
9
+ from structlog.stdlib import BoundLogger, get_logger
10
+
11
+ from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
12
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
13
+ from fastapi_factory_utilities.core.services.status.enums import (
14
+ ComponentTypeEnum,
15
+ HealthStatusEnum,
16
+ ReadinessStatusEnum,
17
+ )
18
+ from fastapi_factory_utilities.core.services.status.services import StatusService
19
+ from fastapi_factory_utilities.core.services.status.types import (
20
+ ComponentInstanceType,
21
+ Status,
22
+ )
23
+
24
+ from .builder import ODMBuilder
25
+ from .configs import ODMConfig
26
+ from .depends import depends_odm_client, depends_odm_database
27
+ from .documents import BaseDocument
28
+ from .exceptions import OperationError, UnableToCreateEntityDueToDuplicateKeyError
29
+ from .helpers import PersistedEntity
30
+ from .repositories import AbstractRepository
31
+
32
+ _logger: BoundLogger = get_logger()
33
+
34
+
35
+ class ODMPlugin(PluginAbstract):
36
+ """ODM plugin."""
37
+
38
+ def __init__(
39
+ self, document_models: list[type[Document]] | None = None, odm_config: ODMConfig | None = None
40
+ ) -> None:
41
+ """Initialize the ODM plugin."""
42
+ super().__init__()
43
+ self._component_instance: ComponentInstanceType | None = None
44
+ self._monitoring_subject: Subject[Status] | None = None
45
+ self._document_models: list[type[Document]] | None = document_models
46
+ self._odm_config: ODMConfig | None = odm_config
47
+ self._odm_client: AsyncIOMotorClient[Any] | None = None
48
+ self._odm_database: AsyncIOMotorDatabase[Any] | None = None
49
+
50
+ def set_application(self, application: ApplicationAbstractProtocol) -> Self:
51
+ """Set the application."""
52
+ self._document_models = self._document_models or application.ODM_DOCUMENT_MODELS
53
+ return super().set_application(application)
54
+
55
+ def on_load(self) -> None:
56
+ """Actions to perform on load for the ODM plugin."""
57
+ # Configure the pymongo logger to INFO level
58
+
59
+ pymongo_logger: Logger = getLogger("pymongo")
60
+ pymongo_logger.setLevel(INFO)
61
+ _logger.debug("ODM plugin loaded.")
62
+
63
+ def _setup_status(self) -> None:
64
+ assert self._application is not None
65
+ status_service: StatusService = self._application.get_status_service()
66
+ self._component_instance = ComponentInstanceType(
67
+ component_type=ComponentTypeEnum.DATABASE, identifier="MongoDB"
68
+ )
69
+ self._monitoring_subject = status_service.register_component_instance(
70
+ component_instance=self._component_instance
71
+ )
72
+
73
+ async def _setup_beanie(self) -> None:
74
+ assert self._application is not None
75
+ assert self._odm_database is not None
76
+ assert self._document_models is not None
77
+ assert self._monitoring_subject is not None
78
+ # TODO: Find a better way to initialize beanie with the document models of the concrete application
79
+ # through an hook in the application, a dynamis import ?
80
+ try:
81
+ await init_beanie(
82
+ database=self._odm_database,
83
+ document_models=self._document_models,
84
+ )
85
+ except Exception as exception: # pylint: disable=broad-except
86
+ _logger.error(f"ODM plugin failed to start. {exception}")
87
+ # TODO: Report the error to the status_service
88
+ # this will report the application as unhealthy
89
+ self._monitoring_subject.on_next(
90
+ value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
91
+ )
92
+
93
+ async def on_startup(self) -> None:
94
+ """Actions to perform on startup for the ODM plugin."""
95
+ assert self._application is not None
96
+ self._setup_status()
97
+ assert self._monitoring_subject is not None
98
+ assert self._component_instance is not None
99
+
100
+ try:
101
+ odm_factory: ODMBuilder = ODMBuilder(application=self._application, odm_config=self._odm_config).build_all()
102
+ await odm_factory.wait_ping()
103
+ self._odm_database = odm_factory.odm_database
104
+ self._odm_client = odm_factory.odm_client
105
+ except Exception as exception: # pylint: disable=broad-except
106
+ _logger.error(f"ODM plugin failed to start. {exception}")
107
+ # TODO: Report the error to the status_service
108
+ # this will report the application as unhealthy
109
+ self._monitoring_subject.on_next(
110
+ value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
111
+ )
112
+ return None
113
+
114
+ if self._odm_database is None or self._odm_client is None:
115
+ _logger.error(
116
+ f"ODM plugin failed to start. Database: {odm_factory.odm_database} - Client: {odm_factory.odm_client}"
117
+ )
118
+ # TODO: Report the error to the status_service
119
+ # this will report the application as unhealthy
120
+ self._monitoring_subject.on_next(
121
+ value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
122
+ )
123
+ return None
124
+
125
+ self._add_to_state(key="odm_client", value=odm_factory.odm_client)
126
+ self._add_to_state(key="odm_database", value=odm_factory.odm_database)
127
+
128
+ await self._setup_beanie()
129
+
130
+ _logger.info(
131
+ f"ODM plugin started. Database: {self._odm_database.name} - "
132
+ f"Client: {self._odm_client.address} - "
133
+ f"Document models: {self._application.ODM_DOCUMENT_MODELS}"
134
+ )
135
+
136
+ self._monitoring_subject.on_next(
137
+ value=Status(health=HealthStatusEnum.HEALTHY, readiness=ReadinessStatusEnum.READY)
138
+ )
139
+
140
+ async def on_shutdown(self) -> None:
141
+ """Actions to perform on shutdown for the ODM plugin."""
142
+ if self._odm_client is not None:
143
+ self._odm_client.close()
144
+ _logger.debug("ODM plugin shutdown.")
145
+
146
+
147
+ __all__: list[str] = [
148
+ "AbstractRepository",
149
+ "BaseDocument",
150
+ "OperationError",
151
+ "PersistedEntity",
152
+ "UnableToCreateEntityDueToDuplicateKeyError",
153
+ "depends_odm_client",
154
+ "depends_odm_database",
155
+ ]
@@ -268,6 +268,7 @@ class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
268
268
  lazy_parse=lazy_parse,
269
269
  nesting_depth=nesting_depth,
270
270
  nesting_depths_per_field=nesting_depths_per_field,
271
+ **pymongo_kwargs,
271
272
  ).to_list()
272
273
  except PyMongoError as error:
273
274
  raise OperationError(f"Failed to find documents: {error}") from error
@@ -1,130 +1,17 @@
1
1
  """OpenTelemetry Plugin Module."""
2
2
 
3
- import asyncio
4
- from typing import cast
5
-
6
- from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
7
- from opentelemetry.instrumentation.fastapi import ( # pyright: ignore[reportMissingTypeStubs]
8
- FastAPIInstrumentor,
9
- )
10
- from opentelemetry.sdk.metrics import MeterProvider
11
- from opentelemetry.sdk.trace import TracerProvider
12
- from structlog.stdlib import BoundLogger, get_logger
13
-
14
- from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
15
-
16
- from .builder import OpenTelemetryPluginBuilder
17
- from .configs import OpenTelemetryConfig
3
+ from .configs import OpenTelemetryConfig, OpenTelemetryMeterConfig, OpenTelemetryTracerConfig
18
4
  from .exceptions import OpenTelemetryPluginBaseException, OpenTelemetryPluginConfigError
5
+ from .plugins import OpenTelemetryPlugin, depends_meter_provider, depends_otel_config, depends_tracer_provider
19
6
 
20
7
  __all__: list[str] = [
21
8
  "OpenTelemetryConfig",
9
+ "OpenTelemetryMeterConfig",
10
+ "OpenTelemetryPlugin",
22
11
  "OpenTelemetryPluginBaseException",
23
12
  "OpenTelemetryPluginConfigError",
24
- "OpenTelemetryPluginBuilder",
13
+ "OpenTelemetryTracerConfig",
14
+ "depends_meter_provider",
15
+ "depends_otel_config",
16
+ "depends_tracer_provider",
25
17
  ]
26
-
27
- _logger: BoundLogger = get_logger()
28
-
29
-
30
- def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
31
- """Check the pre-conditions for the OpenTelemetry plugin.
32
-
33
- Args:
34
- application (BaseApplicationProtocol): The application.
35
-
36
- Returns:
37
- bool: True if the pre-conditions are met, False otherwise.
38
- """
39
- del application
40
- return True
41
-
42
-
43
- def on_load(
44
- application: ApplicationAbstractProtocol,
45
- ) -> None:
46
- """Actions to perform on load for the OpenTelemetry plugin.
47
-
48
- Args:
49
- application (BaseApplicationProtocol): The application.
50
- """
51
- # Build the OpenTelemetry Resources, TracerProvider and MeterProvider
52
- try:
53
- otel_builder: OpenTelemetryPluginBuilder = OpenTelemetryPluginBuilder(application=application).build_all()
54
- except OpenTelemetryPluginBaseException as exception:
55
- _logger.error(f"OpenTelemetry plugin failed to start. {exception}")
56
- return
57
- # Configuration is never None at this point (checked in the builder and raises an exception)
58
- otel_config: OpenTelemetryConfig = cast(OpenTelemetryConfig, otel_builder.config)
59
- # Save as state in the FastAPI application
60
- application.get_asgi_app().state.tracer_provider = otel_builder.tracer_provider
61
- application.get_asgi_app().state.meter_provider = otel_builder.meter_provider
62
- application.get_asgi_app().state.otel_config = otel_config
63
- # Instrument the FastAPI application
64
- FastAPIInstrumentor.instrument_app( # pyright: ignore[reportUnknownMemberType]
65
- app=application.get_asgi_app(),
66
- tracer_provider=otel_builder.tracer_provider,
67
- meter_provider=otel_builder.meter_provider,
68
- excluded_urls=otel_config.excluded_urls,
69
- )
70
- # Instrument the AioHttpClient
71
- AioHttpClientInstrumentor().instrument( # pyright: ignore[reportUnknownMemberType]
72
- tracer_provider=otel_builder.tracer_provider,
73
- meter_provider=otel_builder.meter_provider,
74
- )
75
-
76
- _logger.debug(f"OpenTelemetry plugin loaded. {otel_config.activate=}")
77
-
78
-
79
- async def on_startup(
80
- application: ApplicationAbstractProtocol,
81
- ) -> None:
82
- """Actions to perform on startup for the OpenTelemetry plugin.
83
-
84
- Args:
85
- application (BaseApplicationProtocol): The application.
86
-
87
- Returns:
88
- None
89
- """
90
- del application
91
- _logger.debug("OpenTelemetry plugin started.")
92
-
93
-
94
- async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
95
- """Actions to perform on shutdown for the OpenTelemetry plugin.
96
-
97
- Args:
98
- application (BaseApplicationProtocol): The application.
99
-
100
- Returns:
101
- None
102
- """
103
- tracer_provider: TracerProvider = application.get_asgi_app().state.tracer_provider
104
- meter_provider: MeterProvider = application.get_asgi_app().state.meter_provider
105
- otel_config: OpenTelemetryConfig = application.get_asgi_app().state.otel_config
106
-
107
- seconds_to_ms_multiplier: int = 1000
108
-
109
- async def close_tracer_provider() -> None:
110
- """Close the tracer provider."""
111
- tracer_provider.force_flush(timeout_millis=otel_config.closing_timeout * seconds_to_ms_multiplier)
112
- # No Delay for the shutdown of the tracer provider
113
- tracer_provider.shutdown()
114
-
115
- async def close_meter_provider() -> None:
116
- """Close the meter provider.
117
-
118
- Split the timeout in half for the flush and shutdown.
119
- """
120
- meter_provider.force_flush(timeout_millis=int(otel_config.closing_timeout / 2) * seconds_to_ms_multiplier)
121
- meter_provider.shutdown(timeout_millis=int(otel_config.closing_timeout / 2) * seconds_to_ms_multiplier)
122
-
123
- _logger.debug("OpenTelemetry plugin stop requested. Flushing and closing...")
124
-
125
- await asyncio.gather(
126
- close_tracer_provider(),
127
- close_meter_provider(),
128
- )
129
-
130
- _logger.debug("OpenTelemetry plugin closed.")
@@ -0,0 +1,85 @@
1
+ """Instruments for the OpenTelemetry plugin."""
2
+
3
+ # pyright: reportMissingTypeStubs=false
4
+
5
+ from collections.abc import Callable
6
+ from importlib.util import find_spec
7
+ from typing import Any
8
+
9
+ from opentelemetry.sdk.metrics import MeterProvider
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+
12
+ from fastapi_factory_utilities.core.plugins.opentelemetry_plugin.configs import OpenTelemetryConfig
13
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
14
+
15
+
16
+ def instrument_fastapi(
17
+ application: ApplicationAbstractProtocol,
18
+ config: OpenTelemetryConfig,
19
+ meter_provider: MeterProvider,
20
+ tracer_provider: TracerProvider,
21
+ ) -> None:
22
+ """Instrument the FastAPI application."""
23
+ if find_spec(name="fastapi") and find_spec(name="opentelemetry.instrumentation.fastapi"):
24
+ from opentelemetry.instrumentation.fastapi import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
25
+ FastAPIInstrumentor,
26
+ )
27
+
28
+ excluded_urls_str: str | None = None if len(config.excluded_urls) == 0 else ",".join(config.excluded_urls)
29
+ FastAPIInstrumentor.instrument_app( # pyright: ignore[reportUnknownMemberType]
30
+ app=application.get_asgi_app(),
31
+ tracer_provider=tracer_provider,
32
+ meter_provider=meter_provider,
33
+ excluded_urls=excluded_urls_str,
34
+ )
35
+
36
+
37
+ def instrument_aiohttp(
38
+ application: ApplicationAbstractProtocol, # pylint: disable=unused-argument
39
+ config: OpenTelemetryConfig, # pylint: disable=unused-argument
40
+ meter_provider: MeterProvider,
41
+ tracer_provider: TracerProvider,
42
+ ) -> None:
43
+ """Instrument the Aiohttp application.
44
+
45
+ Args:
46
+ application (ApplicationAbstractProtocol): The application.
47
+ config (OpenTelemetryConfig): The configuration.
48
+ meter_provider (MeterProvider): The meter provider.
49
+ tracer_provider (TracerProvider): The tracer provider.
50
+
51
+ Returns:
52
+ None
53
+ """
54
+ if find_spec(name="aiohttp") and find_spec(name="opentelemetry.instrumentation.aiohttp_client"):
55
+ from opentelemetry.instrumentation.aiohttp_client import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
56
+ AioHttpClientInstrumentor,
57
+ )
58
+
59
+ AioHttpClientInstrumentor().instrument( # pyright: ignore[reportUnknownMemberType]
60
+ tracer_provider=tracer_provider,
61
+ meter_provider=meter_provider,
62
+ )
63
+
64
+
65
+ def instrument_aio_pika(
66
+ application: ApplicationAbstractProtocol, # pylint: disable=unused-argument
67
+ config: OpenTelemetryConfig, # pylint: disable=unused-argument
68
+ meter_provider: MeterProvider,
69
+ tracer_provider: TracerProvider,
70
+ ) -> None:
71
+ """Instrument the AioPika application."""
72
+ if find_spec(name="aio_pika") and find_spec(name="opentelemetry.instrumentation.aio_pika"):
73
+ from opentelemetry.instrumentation.aio_pika import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
74
+ AioPikaInstrumentor,
75
+ )
76
+
77
+ AioPikaInstrumentor().instrument( # pyright: ignore[reportUnknownMemberType]
78
+ tracer_provider=tracer_provider,
79
+ meter_provider=meter_provider,
80
+ )
81
+
82
+
83
+ INSTRUMENTS: list[Callable[..., Any]] = [instrument_fastapi, instrument_aiohttp, instrument_aio_pika]
84
+
85
+ __all__: list[str] = ["INSTRUMENTS"]
@@ -0,0 +1,137 @@
1
+ """Provides the OpenTelemetry plugin."""
2
+
3
+ import asyncio
4
+ from typing import Self, cast
5
+
6
+ from fastapi import Request
7
+ from opentelemetry.sdk.metrics import MeterProvider
8
+ from opentelemetry.sdk.trace import TracerProvider
9
+ from structlog.stdlib import BoundLogger, get_logger
10
+
11
+ from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
12
+
13
+ from .builder import OpenTelemetryPluginBuilder
14
+ from .configs import OpenTelemetryConfig
15
+ from .exceptions import OpenTelemetryPluginBaseException, OpenTelemetryPluginConfigError
16
+ from .instruments import INSTRUMENTS
17
+
18
+ __all__: list[str] = [
19
+ "OpenTelemetryConfig",
20
+ "OpenTelemetryPluginBaseException",
21
+ "OpenTelemetryPluginBuilder",
22
+ "OpenTelemetryPluginConfigError",
23
+ ]
24
+
25
+ _logger: BoundLogger = get_logger()
26
+
27
+
28
+ class OpenTelemetryPlugin(PluginAbstract):
29
+ """OpenTelemetry plugin."""
30
+
31
+ SECONDS_TO_MS_MULTIPLIER: int = 1000
32
+
33
+ def __init__(self) -> None:
34
+ """Initialize the OpenTelemetry plugin."""
35
+ super().__init__()
36
+ self._otel_config: OpenTelemetryConfig | None = None
37
+ self._tracer_provider: TracerProvider | None = None
38
+ self._meter_provider: MeterProvider | None = None
39
+
40
+ def _build(self) -> Self:
41
+ """Build the OpenTelemetry plugin."""
42
+ assert self._application is not None
43
+ # Build the OpenTelemetry Resources, TracerProvider and MeterProvider
44
+ try:
45
+ otel_builder: OpenTelemetryPluginBuilder = OpenTelemetryPluginBuilder(
46
+ application=self._application
47
+ ).build_all()
48
+ except OpenTelemetryPluginBaseException as exception:
49
+ _logger.error(f"OpenTelemetry plugin failed to start. {exception}")
50
+ raise
51
+ # Configuration is never None at this point (checked in the builder and raises an exception)
52
+ self._otel_config = cast(OpenTelemetryConfig, otel_builder.config)
53
+ self._tracer_provider = otel_builder.tracer_provider
54
+ self._meter_provider = otel_builder.meter_provider
55
+ return self
56
+
57
+ def _instrument(self) -> None:
58
+ """Instrument the FastAPI application."""
59
+ assert self._application is not None
60
+ assert self._tracer_provider is not None
61
+ assert self._meter_provider is not None
62
+ assert self._otel_config is not None
63
+
64
+ for instrument in INSTRUMENTS:
65
+ instrument(self._application, self._otel_config, self._meter_provider, self._tracer_provider)
66
+
67
+ def on_load(self) -> None:
68
+ """On load."""
69
+ assert self._application is not None
70
+ # Build the OpenTelemetry Resources, TracerProvider and MeterProvider
71
+ self._build()
72
+ assert self._tracer_provider is not None
73
+ assert self._meter_provider is not None
74
+ assert self._otel_config is not None
75
+ self._add_to_state(key="tracer_provider", value=self._tracer_provider)
76
+ self._add_to_state(key="meter_provider", value=self._meter_provider)
77
+ self._add_to_state(key="otel_config", value=self._otel_config)
78
+ # Instrument the FastAPI application and AioHttpClient, ...
79
+ self._instrument()
80
+ _logger.debug(f"OpenTelemetry plugin loaded. {self._otel_config.activate=}")
81
+
82
+ async def on_startup(self) -> None:
83
+ """On startup."""
84
+ pass
85
+
86
+ async def close_tracer_provider(self) -> None:
87
+ """Close the tracer provider."""
88
+ assert self._tracer_provider is not None
89
+ assert self._otel_config is not None
90
+ self._tracer_provider.force_flush(
91
+ timeout_millis=self._otel_config.closing_timeout * self.SECONDS_TO_MS_MULTIPLIER
92
+ )
93
+ # No Delay for the shutdown of the tracer provider
94
+ try:
95
+ self._tracer_provider.shutdown()
96
+ except Exception as exception: # pylint: disable=broad-exception-caught
97
+ _logger.error("OpenTelemetry plugin failed to close the tracer provider.", error=exception)
98
+
99
+ async def close_meter_provider(self) -> None:
100
+ """Close the meter provider."""
101
+ assert self._meter_provider is not None
102
+ assert self._otel_config is not None
103
+ self._meter_provider.force_flush(
104
+ timeout_millis=self._otel_config.closing_timeout * self.SECONDS_TO_MS_MULTIPLIER
105
+ )
106
+ try:
107
+ self._meter_provider.shutdown(
108
+ timeout_millis=self._otel_config.closing_timeout * self.SECONDS_TO_MS_MULTIPLIER
109
+ )
110
+ except Exception as exception: # pylint: disable=broad-exception-caught
111
+ _logger.error("OpenTelemetry plugin failed to close the meter provider.", error=exception)
112
+
113
+ async def on_shutdown(self) -> None:
114
+ """On shutdown."""
115
+ _logger.debug("OpenTelemetry plugin stop requested. Flushing and closing...")
116
+
117
+ await asyncio.gather(
118
+ self.close_tracer_provider(),
119
+ self.close_meter_provider(),
120
+ )
121
+
122
+ _logger.debug("OpenTelemetry plugin closed.")
123
+
124
+
125
+ def depends_tracer_provider(request: Request) -> TracerProvider:
126
+ """Get the tracer provider."""
127
+ return request.app.state.tracer_provider
128
+
129
+
130
+ def depends_meter_provider(request: Request) -> MeterProvider:
131
+ """Get the meter provider."""
132
+ return request.app.state.meter_provider
133
+
134
+
135
+ def depends_otel_config(request: Request) -> OpenTelemetryConfig:
136
+ """Get the OpenTelemetry config."""
137
+ return request.app.state.otel_config
@@ -0,0 +1,31 @@
1
+ """Taskiq Plugin Module."""
2
+
3
+ from importlib.util import find_spec
4
+
5
+ from .configs import RedisCredentialsConfig
6
+ from .depends import depends_scheduler_component
7
+ from .exceptions import TaskiqPluginBaseError
8
+ from .plugin import TaskiqPlugin
9
+ from .schedulers import SchedulerComponent
10
+
11
+ __all__: list[str] = [ # pylint: disable=invalid-name
12
+ "RedisCredentialsConfig",
13
+ "SchedulerComponent",
14
+ "TaskiqPlugin",
15
+ "TaskiqPluginBaseError",
16
+ "depends_scheduler_component",
17
+ ]
18
+
19
+ if find_spec("beanie") is not None:
20
+ from .depends import depends_odm_database
21
+
22
+ __all__ += [ # pylint: disable=invalid-name
23
+ "depends_odm_database",
24
+ ]
25
+
26
+ if find_spec("aio_pika") is not None:
27
+ from .depends import depends_aiopika_robust_connection
28
+
29
+ __all__ += [ # pylint: disable=invalid-name
30
+ "depends_aiopika_robust_connection",
31
+ ]
@@ -0,0 +1,12 @@
1
+ """Provides the configurations for the Taskiq plugin."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ class RedisCredentialsConfig(BaseModel):
9
+ """Redis credentials config."""
10
+
11
+ model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
12
+ url: str
@@ -0,0 +1,51 @@
1
+ """Provides the dependencies for the Taskiq plugin."""
2
+
3
+ from importlib.util import find_spec
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from fastapi import Request
7
+ from taskiq import TaskiqDepends
8
+
9
+ if TYPE_CHECKING:
10
+ from .schedulers import SchedulerComponent
11
+
12
+ DEPENDS_SCHEDULER_COMPONENT_KEY: str = "scheduler_component"
13
+
14
+
15
+ def depends_scheduler_component(
16
+ request: Request = TaskiqDepends(),
17
+ ) -> "SchedulerComponent":
18
+ """Dependency injection for the scheduler component."""
19
+ return getattr(request.app.state, DEPENDS_SCHEDULER_COMPONENT_KEY)
20
+
21
+
22
+ if find_spec("beanie") is not None:
23
+ from motor.motor_asyncio import AsyncIOMotorDatabase
24
+
25
+ def depends_odm_database(request: Request = TaskiqDepends()) -> AsyncIOMotorDatabase[Any]:
26
+ """Acquire the ODM database from the request.
27
+
28
+ Args:
29
+ request (Request): The request.
30
+
31
+ Returns:
32
+ AsyncIOMotorClient: The ODM database.
33
+ """
34
+ return request.app.state.odm_database
35
+
36
+
37
+ if find_spec("aio_pika") is not None:
38
+ from aio_pika.abc import AbstractRobustConnection
39
+
40
+ from fastapi_factory_utilities.core.plugins.aiopika.depends import DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY
41
+
42
+ def depends_aiopika_robust_connection(request: Request = TaskiqDepends()) -> AbstractRobustConnection:
43
+ """Acquire the Aiopika robust connection from the request.
44
+
45
+ Args:
46
+ request (Request): The request.
47
+
48
+ Returns:
49
+ AbstractRobustConnection: The Aiopika robust connection.
50
+ """
51
+ return getattr(request.app.state, DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY)