fastapi-factory-utilities 0.3.3__py3-none-any.whl → 0.8.2__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.
Files changed (75) 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 +19 -35
  6. fastapi_factory_utilities/core/app/config.py +2 -0
  7. fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
  8. fastapi_factory_utilities/core/exceptions.py +64 -28
  9. fastapi_factory_utilities/core/plugins/__init__.py +2 -31
  10. fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
  11. fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
  12. fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
  13. fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
  14. fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
  15. fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
  16. fastapi_factory_utilities/core/plugins/aiopika/exchange.py +69 -0
  17. fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
  18. fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
  19. fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
  20. fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
  21. fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
  22. fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
  23. fastapi_factory_utilities/core/plugins/aiopika/queue.py +88 -0
  24. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +14 -157
  25. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +4 -3
  26. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
  27. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +1 -1
  28. fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
  29. fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
  30. fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +12 -23
  31. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -115
  32. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
  33. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
  34. fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +31 -0
  35. fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
  36. fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
  37. fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
  38. fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
  39. fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
  40. fastapi_factory_utilities/core/protocols.py +1 -54
  41. fastapi_factory_utilities/core/security/__init__.py +5 -0
  42. fastapi_factory_utilities/core/security/abstracts.py +42 -0
  43. fastapi_factory_utilities/core/security/jwt/__init__.py +41 -0
  44. fastapi_factory_utilities/core/security/jwt/configs.py +32 -0
  45. fastapi_factory_utilities/core/security/jwt/decoders.py +130 -0
  46. fastapi_factory_utilities/core/security/jwt/exceptions.py +23 -0
  47. fastapi_factory_utilities/core/security/jwt/objects.py +107 -0
  48. fastapi_factory_utilities/core/security/jwt/services.py +176 -0
  49. fastapi_factory_utilities/core/security/jwt/stores.py +43 -0
  50. fastapi_factory_utilities/core/security/jwt/types.py +9 -0
  51. fastapi_factory_utilities/core/security/jwt/verifiers.py +46 -0
  52. fastapi_factory_utilities/core/security/kratos.py +53 -33
  53. fastapi_factory_utilities/core/services/hydra/__init__.py +20 -0
  54. fastapi_factory_utilities/core/services/hydra/exceptions.py +15 -0
  55. fastapi_factory_utilities/core/services/hydra/objects.py +26 -0
  56. fastapi_factory_utilities/core/services/hydra/services.py +200 -0
  57. fastapi_factory_utilities/core/services/status/__init__.py +2 -2
  58. fastapi_factory_utilities/core/services/status/exceptions.py +1 -1
  59. fastapi_factory_utilities/core/utils/status.py +2 -1
  60. fastapi_factory_utilities/core/utils/yaml_reader.py +1 -1
  61. fastapi_factory_utilities/example/app.py +15 -5
  62. fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
  63. fastapi_factory_utilities/example/models/books/__init__.py +1 -1
  64. {fastapi_factory_utilities-0.3.3.dist-info → fastapi_factory_utilities-0.8.2.dist-info}/METADATA +21 -15
  65. fastapi_factory_utilities-0.8.2.dist-info/RECORD +111 -0
  66. {fastapi_factory_utilities-0.3.3.dist-info → fastapi_factory_utilities-0.8.2.dist-info}/WHEEL +1 -1
  67. fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
  68. fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
  69. fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
  70. fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
  71. fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
  72. fastapi_factory_utilities/core/security/jwt.py +0 -158
  73. fastapi_factory_utilities-0.3.3.dist-info/RECORD +0 -78
  74. {fastapi_factory_utilities-0.3.3.dist-info → fastapi_factory_utilities-0.8.2.dist-info}/entry_points.txt +0 -0
  75. {fastapi_factory_utilities-0.3.3.dist-info → fastapi_factory_utilities-0.8.2.dist-info/licenses}/LICENSE +0 -0
@@ -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)
@@ -0,0 +1,13 @@
1
+ """Provides the exceptions for the Taskiq plugin."""
2
+
3
+ from typing import Any
4
+
5
+ from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
6
+
7
+
8
+ class TaskiqPluginBaseError(FastAPIFactoryUtilitiesError):
9
+ """Base class for all exceptions raised by the Taskiq plugin."""
10
+
11
+ def __init__(self, message: str, **kwargs: Any) -> None:
12
+ """Initialize the Taskiq plugin base exception."""
13
+ super().__init__(message, **kwargs)
@@ -0,0 +1,41 @@
1
+ """Provides the Taskiq plugin."""
2
+
3
+ from collections.abc import Callable
4
+
5
+ from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
6
+
7
+ from .configs import RedisCredentialsConfig
8
+ from .depends import DEPENDS_SCHEDULER_COMPONENT_KEY
9
+ from .schedulers import SchedulerComponent
10
+
11
+
12
+ class TaskiqPlugin(PluginAbstract):
13
+ """Taskiq plugin."""
14
+
15
+ def __init__(
16
+ self, redis_credentials_config: RedisCredentialsConfig, register_hook: Callable[[SchedulerComponent], None]
17
+ ) -> None:
18
+ """Initialize the Taskiq plugin."""
19
+ super().__init__()
20
+ self._redis_credentials_config: RedisCredentialsConfig = redis_credentials_config
21
+ self._register_hook: Callable[[SchedulerComponent], None] = register_hook
22
+ self._scheduler_component: SchedulerComponent = SchedulerComponent()
23
+
24
+ def on_load(self) -> None:
25
+ """On load."""
26
+ assert self._application is not None
27
+ self._scheduler_component.configure(
28
+ redis_connection_string=self._redis_credentials_config.url, app=self._application.get_asgi_app()
29
+ )
30
+ self._add_to_state(key=DEPENDS_SCHEDULER_COMPONENT_KEY, value=self._scheduler_component)
31
+ self._register_hook(self._scheduler_component)
32
+
33
+ async def on_startup(self) -> None:
34
+ """On startup."""
35
+ assert self._application is not None
36
+ await self._scheduler_component.startup(app=self._application.get_asgi_app())
37
+
38
+ async def on_shutdown(self) -> None:
39
+ """On shutdown."""
40
+ assert self._application is not None
41
+ await self._scheduler_component.shutdown()
@@ -0,0 +1,187 @@
1
+ """Scheduler module for fastapi_factory_utilities.
2
+
3
+ This module provides components and utilities for scheduling tasks using Taskiq, FastAPI, and Redis.
4
+ It enables registration, configuration, and management of scheduled tasks in FastAPI applications.
5
+ """
6
+
7
+ import asyncio
8
+ from collections.abc import Coroutine
9
+ from typing import Any, Self, cast
10
+
11
+ import taskiq_fastapi
12
+ from fastapi import FastAPI
13
+ from structlog.stdlib import get_logger
14
+ from taskiq import (
15
+ AsyncBroker,
16
+ AsyncTaskiqDecoratedTask,
17
+ ScheduleSource,
18
+ TaskiqScheduler,
19
+ )
20
+ from taskiq.api import run_receiver_task, run_scheduler_task
21
+ from taskiq.scheduler.created_schedule import CreatedSchedule
22
+ from taskiq.scheduler.scheduled_task import ScheduledTask
23
+ from taskiq_redis import (
24
+ ListRedisScheduleSource,
25
+ RedisAsyncResultBackend,
26
+ RedisStreamBroker,
27
+ )
28
+
29
+ _logger = get_logger(__package__)
30
+
31
+
32
+ class SchedulerComponent:
33
+ """Scheduler component."""
34
+
35
+ NAME_SUFFIX: str = "tiktok_integration"
36
+
37
+ def __init__(self) -> None:
38
+ """Initialize the scheduler component."""
39
+ self._result_backend: RedisAsyncResultBackend[Any] | None = None
40
+ self._stream_broker: RedisStreamBroker | None = None
41
+ self._scheduler: TaskiqScheduler | None = None
42
+ self._scheduler_source: ListRedisScheduleSource | None = None
43
+ self._dyn_task: AsyncTaskiqDecoratedTask[Any, Any] | None = None
44
+ self._schedule_cron: ScheduledTask | None = None
45
+ self._schedulers_tasks: dict[str, AsyncTaskiqDecoratedTask[Any, Any]] = {}
46
+
47
+ def register_task(self, task: Coroutine[Any, Any, Any], task_name: str) -> None:
48
+ """Register a task.
49
+
50
+ Args:
51
+ task: The task to register.
52
+ task_name: The name of the task.
53
+
54
+ Raises:
55
+ ValueError: If the task is already registered.
56
+ ValueError: If the stream broker is not initialized.
57
+ """
58
+ if self._stream_broker is None:
59
+ raise ValueError("Stream broker is not initialized")
60
+
61
+ if task_name in self._schedulers_tasks:
62
+ raise ValueError(f"Task {task_name} already registered")
63
+
64
+ self._schedulers_tasks[task_name] = self._stream_broker.register_task(task, task_name) # type: ignore
65
+
66
+ def get_task(self, task_name: str) -> AsyncTaskiqDecoratedTask[Any, Any]:
67
+ """Get a task.
68
+
69
+ Args:
70
+ task_name: The name of the task.
71
+
72
+ Returns:
73
+ AsyncTaskiqDecoratedTask: The task.
74
+
75
+ Raises:
76
+ ValueError: If the task is not registered.
77
+ """
78
+ if task_name not in self._schedulers_tasks:
79
+ raise ValueError(f"Task {task_name} not registered")
80
+ return self._schedulers_tasks[task_name]
81
+
82
+ def configure(self, redis_connection_string: str, app: FastAPI) -> Self:
83
+ """Configure the scheduler component."""
84
+ self._result_backend = RedisAsyncResultBackend(
85
+ redis_url=redis_connection_string,
86
+ prefix_str=f"velmios_taskiq_result_backend_{self.NAME_SUFFIX}",
87
+ result_ex_time=120,
88
+ )
89
+ self._stream_broker = RedisStreamBroker(
90
+ url=redis_connection_string,
91
+ queue_name=f"velmios_taskiq_stream_broker_{self.NAME_SUFFIX}",
92
+ consumer_group_name=f"velmios_taskiq_consumer_group_{self.NAME_SUFFIX}",
93
+ ).with_result_backend(self._result_backend)
94
+
95
+ taskiq_fastapi.populate_dependency_context(self._stream_broker, app)
96
+
97
+ self._scheduler_source = ListRedisScheduleSource(
98
+ url=redis_connection_string,
99
+ prefix=f"velmios_taskiq_schedule_source_{self.NAME_SUFFIX}",
100
+ )
101
+
102
+ self._scheduler = TaskiqScheduler(
103
+ broker=self._stream_broker,
104
+ sources=[self._scheduler_source],
105
+ )
106
+
107
+ return self
108
+
109
+ async def startup(self, app: FastAPI) -> None:
110
+ """Start the scheduler."""
111
+ if self._result_backend is None:
112
+ raise ValueError("Result backend is not initialized")
113
+ if self._stream_broker is None:
114
+ raise ValueError("Stream broker is not initialized")
115
+ if self._scheduler is None:
116
+ raise ValueError("Scheduler is not initialized")
117
+ if self._scheduler_source is None:
118
+ raise ValueError("Scheduler source is not initialized")
119
+
120
+ _logger.info("Starting scheduler")
121
+ await self._result_backend.startup()
122
+ await self._stream_broker.startup()
123
+ await self._scheduler.startup()
124
+ _logger.info("Scheduler started")
125
+ _logger.info("Scheduling task")
126
+ schedules: list[ScheduledTask] = await self._scheduler_source.get_schedules()
127
+ _logger.info("Schedules retrieved", schedules=schedules)
128
+
129
+ self._schedule_cron = next(filter(lambda x: x.task_name == "heartbeat", schedules), None)
130
+
131
+ if self._schedule_cron is None:
132
+ _logger.info("No schedules found, scheduling task")
133
+ self._dyn_task = self.get_task("heartbeat")
134
+ task_created: CreatedSchedule[Any] = await self._dyn_task.schedule_by_cron(
135
+ source=self._scheduler_source, cron="* * * * *", msg="every minute"
136
+ )
137
+ self._schedule_cron = task_created.task
138
+ _logger.info("Task scheduled")
139
+ else:
140
+ _logger.info("Schedules found, skipping scheduling")
141
+
142
+ _logger.info("Starting worker and scheduler tasks")
143
+ taskiq_fastapi.populate_dependency_context(self._stream_broker, app, app.state) # type: ignore
144
+ self._worker_task: asyncio.Task[None] = asyncio.create_task(run_receiver_task(self._stream_broker))
145
+ self._scheduler_task: asyncio.Task[None] = asyncio.create_task(run_scheduler_task(self._scheduler))
146
+ _logger.info("Worker and scheduler tasks started")
147
+
148
+ async def shutdown(self) -> None:
149
+ """Stop the scheduler."""
150
+ _logger.info("Stopping worker")
151
+ self._worker_task.cancel()
152
+ self._scheduler_task.cancel()
153
+ try:
154
+ await self._worker_task
155
+ except (asyncio.CancelledError, RuntimeError) as e:
156
+ _logger.info("Worker task cancelled", error=e)
157
+ try:
158
+ await self._scheduler_task
159
+ except (asyncio.CancelledError, RuntimeError) as e:
160
+ _logger.info("Scheduler task cancelled", error=e)
161
+
162
+ while not self._worker_task.done() or not self._scheduler_task.done():
163
+ await asyncio.sleep(0.1)
164
+
165
+ _logger.info("Stopping scheduler")
166
+ if self._scheduler is not None:
167
+ await self._scheduler.shutdown()
168
+ if self._stream_broker is not None:
169
+ await self._stream_broker.shutdown()
170
+ if self._result_backend is not None:
171
+ await self._result_backend.shutdown()
172
+ _logger.info("Scheduler stopped")
173
+
174
+ @property
175
+ def scheduler(self) -> TaskiqScheduler:
176
+ """Get the scheduler."""
177
+ return cast(TaskiqScheduler, self._scheduler)
178
+
179
+ @property
180
+ def broker(self) -> AsyncBroker:
181
+ """Get the broker."""
182
+ return cast(AsyncBroker, self._stream_broker)
183
+
184
+ @property
185
+ def scheduler_source(self) -> ScheduleSource:
186
+ """Get the scheduler source."""
187
+ return cast(ScheduleSource, self._scheduler_source)
@@ -1,17 +1,15 @@
1
1
  """Protocols for the base application."""
2
2
 
3
3
  from abc import abstractmethod
4
- from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
4
+ from typing import TYPE_CHECKING, ClassVar, Protocol
5
5
 
6
6
  from beanie import Document
7
7
  from fastapi import FastAPI
8
8
 
9
- from fastapi_factory_utilities.core.plugins import PluginsEnum
10
9
  from fastapi_factory_utilities.core.services.status.services import StatusService
11
10
 
12
11
  if TYPE_CHECKING:
13
12
  from fastapi_factory_utilities.core.app.config import RootConfig
14
- from fastapi_factory_utilities.core.plugins import PluginState
15
13
 
16
14
 
17
15
  class ApplicationAbstractProtocol(Protocol):
@@ -21,8 +19,6 @@ class ApplicationAbstractProtocol(Protocol):
21
19
 
22
20
  ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]]
23
21
 
24
- DEFAULT_PLUGINS_ACTIVATED: ClassVar[list[PluginsEnum]]
25
-
26
22
  @abstractmethod
27
23
  def get_config(self) -> "RootConfig":
28
24
  """Get the application configuration."""
@@ -34,52 +30,3 @@ class ApplicationAbstractProtocol(Protocol):
34
30
  @abstractmethod
35
31
  def get_status_service(self) -> StatusService:
36
32
  """Get the status service."""
37
-
38
-
39
- @runtime_checkable
40
- class PluginProtocol(Protocol):
41
- """Defines the protocol for the plugins."""
42
-
43
- @abstractmethod
44
- def pre_conditions_check(self, application: ApplicationAbstractProtocol) -> bool:
45
- """Check the pre-conditions for the plugin.
46
-
47
- Args:
48
- application (BaseApplicationProtocol): The application.
49
-
50
- Returns:
51
- bool: True if the pre-conditions are met, False otherwise.
52
- """
53
-
54
- @abstractmethod
55
- def on_load(self, application: ApplicationAbstractProtocol) -> list["PluginState"] | None:
56
- """The actions to perform on load for the plugin.
57
-
58
- Args:
59
- application (BaseApplicationProtocol): The application.
60
-
61
- Returns:
62
- None
63
- """
64
-
65
- @abstractmethod
66
- async def on_startup(self, application: ApplicationAbstractProtocol) -> list["PluginState"] | None:
67
- """The actions to perform on startup for the plugin.
68
-
69
- Args:
70
- application (BaseApplicationProtocol): The application.
71
-
72
- Returns:
73
- None
74
- """
75
-
76
- @abstractmethod
77
- async def on_shutdown(self, application: ApplicationAbstractProtocol) -> None:
78
- """The actions to perform on shutdown for the plugin.
79
-
80
- Args:
81
- application (BaseApplicationProtocol): The application.
82
-
83
- Returns:
84
- None
85
- """
@@ -0,0 +1,5 @@
1
+ """Security module."""
2
+
3
+ from .abstracts import AuthenticationAbstract
4
+
5
+ __all__: list[str] = ["AuthenticationAbstract"]