fastapi-factory-utilities 0.1.0__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 (58) hide show
  1. fastapi_factory_utilities/__main__.py +6 -0
  2. fastapi_factory_utilities/core/__init__.py +1 -0
  3. fastapi_factory_utilities/core/api/__init__.py +25 -0
  4. fastapi_factory_utilities/core/api/tags.py +9 -0
  5. fastapi_factory_utilities/core/api/v1/sys/__init__.py +12 -0
  6. fastapi_factory_utilities/core/api/v1/sys/health.py +53 -0
  7. fastapi_factory_utilities/core/api/v1/sys/readiness.py +53 -0
  8. fastapi_factory_utilities/core/app/__init__.py +19 -0
  9. fastapi_factory_utilities/core/app/base/__init__.py +17 -0
  10. fastapi_factory_utilities/core/app/base/application.py +123 -0
  11. fastapi_factory_utilities/core/app/base/config_abstract.py +78 -0
  12. fastapi_factory_utilities/core/app/base/exceptions.py +25 -0
  13. fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +88 -0
  14. fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +136 -0
  15. fastapi_factory_utilities/core/app/enums.py +11 -0
  16. fastapi_factory_utilities/core/plugins/__init__.py +15 -0
  17. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +97 -0
  18. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +239 -0
  19. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +17 -0
  20. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +31 -0
  21. fastapi_factory_utilities/core/plugins/odm_plugin/exceptions.py +25 -0
  22. fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +172 -0
  23. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +124 -0
  24. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +266 -0
  25. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +103 -0
  26. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/exceptions.py +13 -0
  27. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/helpers.py +42 -0
  28. fastapi_factory_utilities/core/protocols.py +82 -0
  29. fastapi_factory_utilities/core/utils/configs.py +80 -0
  30. fastapi_factory_utilities/core/utils/importlib.py +28 -0
  31. fastapi_factory_utilities/core/utils/log.py +178 -0
  32. fastapi_factory_utilities/core/utils/uvicorn.py +45 -0
  33. fastapi_factory_utilities/core/utils/yaml_reader.py +166 -0
  34. fastapi_factory_utilities/example/__init__.py +11 -0
  35. fastapi_factory_utilities/example/__main__.py +6 -0
  36. fastapi_factory_utilities/example/api/__init__.py +19 -0
  37. fastapi_factory_utilities/example/api/books/__init__.py +5 -0
  38. fastapi_factory_utilities/example/api/books/responses.py +26 -0
  39. fastapi_factory_utilities/example/api/books/routes.py +62 -0
  40. fastapi_factory_utilities/example/app/__init__.py +6 -0
  41. fastapi_factory_utilities/example/app/app.py +37 -0
  42. fastapi_factory_utilities/example/app/config.py +12 -0
  43. fastapi_factory_utilities/example/application.yaml +26 -0
  44. fastapi_factory_utilities/example/entities/books/__init__.py +7 -0
  45. fastapi_factory_utilities/example/entities/books/entities.py +16 -0
  46. fastapi_factory_utilities/example/entities/books/enums.py +16 -0
  47. fastapi_factory_utilities/example/entities/books/types.py +54 -0
  48. fastapi_factory_utilities/example/models/__init__.py +1 -0
  49. fastapi_factory_utilities/example/models/books/__init__.py +6 -0
  50. fastapi_factory_utilities/example/models/books/document.py +20 -0
  51. fastapi_factory_utilities/example/models/books/repository.py +11 -0
  52. fastapi_factory_utilities/example/services/books/__init__.py +5 -0
  53. fastapi_factory_utilities/example/services/books/services.py +167 -0
  54. fastapi_factory_utilities-0.1.0.dist-info/LICENSE +21 -0
  55. fastapi_factory_utilities-0.1.0.dist-info/METADATA +131 -0
  56. fastapi_factory_utilities-0.1.0.dist-info/RECORD +58 -0
  57. fastapi_factory_utilities-0.1.0.dist-info/WHEEL +4 -0
  58. fastapi_factory_utilities-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,80 @@
1
+ """Provides utilities to handle configurations."""
2
+
3
+ from typing import Any, TypeVar
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
8
+ from fastapi_factory_utilities.core.utils.yaml_reader import (
9
+ UnableToReadYamlFileError,
10
+ YamlFileReader,
11
+ )
12
+
13
+ GenericConfigBaseModelType = TypeVar("GenericConfigBaseModelType", bound=BaseModel) # pylint: disable=invalid-name
14
+
15
+
16
+ class ConfigBaseException(BaseException):
17
+ """Base exception for all the configuration exceptions."""
18
+
19
+ pass
20
+
21
+
22
+ class UnableToReadConfigFileError(ConfigBaseException):
23
+ """Exception raised when the configuration file cannot be read.
24
+
25
+ Mainly used when the file is not found or the file is not a YAML file.
26
+ """
27
+
28
+ pass
29
+
30
+
31
+ class ValueErrorConfigError(ConfigBaseException):
32
+ """Exception raised when the configuration object cannot be created.
33
+
34
+ Mainly used when validation fails when creating the configuration object.
35
+ """
36
+
37
+ pass
38
+
39
+
40
+ def build_config_from_file_in_package(
41
+ package_name: str,
42
+ filename: str,
43
+ config_class: type[GenericConfigBaseModelType],
44
+ yaml_base_key: str,
45
+ ) -> GenericConfigBaseModelType:
46
+ """Build a configuration object from a file in a package.
47
+
48
+ Args:
49
+ package_name (str): The package name.
50
+ filename (str): The filename.
51
+ config_class (type[GenericConfigBaseModelType]): The configuration class.
52
+ yaml_base_key (str): The base key in the YAML file.
53
+
54
+ Returns:
55
+ GenericConfigBaseModelType: The configuration object.
56
+
57
+ Raises:
58
+ UnableToReadConfigFileError: If the configuration file cannot be read.
59
+ ValueErrorConfigError: If the configuration file is invalid.
60
+ """
61
+ # Read the application configuration file
62
+ try:
63
+ yaml_file_content: dict[str, Any] = YamlFileReader(
64
+ file_path=get_path_file_in_package(
65
+ filename=filename,
66
+ package=package_name,
67
+ ),
68
+ yaml_base_key=yaml_base_key,
69
+ use_environment_injection=True,
70
+ ).read()
71
+ except (FileNotFoundError, ImportError, UnableToReadYamlFileError) as exception:
72
+ raise UnableToReadConfigFileError("Unable to read the application configuration file.") from exception
73
+
74
+ # Create the application configuration model
75
+ try:
76
+ config: GenericConfigBaseModelType = config_class(**yaml_file_content)
77
+ except ValueError as exception:
78
+ raise ValueErrorConfigError("Unable to create the configuration model.") from exception
79
+
80
+ return config
@@ -0,0 +1,28 @@
1
+ """Provide importlib functions."""
2
+
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+
7
+ def get_path_file_in_package(filename: str, package: str) -> Path:
8
+ """Return Absolute Path of file in package.
9
+
10
+ Args:
11
+ filename (str): Filename to search
12
+ package (str): Package name
13
+
14
+ Returns:
15
+ Traversable: File
16
+
17
+ Raises:
18
+ FileNotFoundError: If file not found
19
+ ImportError: If package not found
20
+
21
+ """
22
+ try:
23
+ path: Path = Path(str(files(package).joinpath(filename)))
24
+ except FileNotFoundError as exception:
25
+ raise FileNotFoundError(f"File {filename} not found in package {package}") from exception
26
+ except ImportError as exception:
27
+ raise ImportError(f"Package {package} not found") from exception
28
+ return path
@@ -0,0 +1,178 @@
1
+ """Provides a function to setup the logging configuration."""
2
+
3
+ import logging
4
+ import sys
5
+ from enum import StrEnum, auto
6
+ from typing import Annotated, Any
7
+
8
+ import structlog
9
+ from pydantic import BaseModel, BeforeValidator
10
+ from structlog.types import EventDict
11
+
12
+ _logger = structlog.getLogger(__package__)
13
+
14
+
15
+ def ensure_logging_level(level: Any) -> int:
16
+ """Ensure the logging level.
17
+
18
+ Args:
19
+ level (Any): The logging level.
20
+
21
+ Returns:
22
+ int: The logging level.
23
+ """
24
+ if isinstance(level, int):
25
+ return level
26
+
27
+ if isinstance(level, str):
28
+ try:
29
+ return getattr(logging, str(level).upper())
30
+ except AttributeError as exception:
31
+ raise ValueError(f"Invalid logging level: {level}") from exception
32
+
33
+ raise ValueError(f"Invalid logging level: {level}")
34
+
35
+
36
+ class LoggingConfig(BaseModel):
37
+ """Logging configuration."""
38
+
39
+ name: str
40
+ level: Annotated[int, BeforeValidator(ensure_logging_level)]
41
+
42
+
43
+ class LogModeEnum(StrEnum):
44
+ """Defines the possible logging modes."""
45
+
46
+ CONSOLE = auto()
47
+ JSON = auto()
48
+
49
+
50
+ # https://github.com/hynek/structlog/issues/35#issuecomment-591321744
51
+ def _rename_event_key(_: Any, __: Any, event_dict: EventDict) -> EventDict: # pylint: disable=invalid-name
52
+ """Renames the `event` key to `message` in the event dictionary.
53
+
54
+ Log entries keep the text message in the `event` field, but Datadog
55
+ uses the `message` field. This processor moves the value from one field to
56
+ the other.
57
+ See https://github.com/hynek/structlog/issues/35#issuecomment-591321744
58
+ """
59
+ event_dict["message"] = event_dict.pop("event")
60
+ return event_dict
61
+
62
+
63
+ def clean_uvicorn_logger() -> None:
64
+ """Cleans the uvicorn loggers."""
65
+ for logger_name in ["uvicorn", "uvicorn.error", "uvicorn.access"]:
66
+ # Clear the log handlers for uvicorn loggers, and enable propagation
67
+ # so the messages are caught by our root logger and formatted correctly
68
+ # by structlog
69
+ logging.getLogger(logger_name).handlers.clear()
70
+ logging.getLogger(logger_name).propagate = True
71
+
72
+
73
+ def _drop_color_message_key(_: Any, __: Any, event_dict: EventDict) -> EventDict: # pylint: disable=invalid-name
74
+ """Cleans the `color_message` key from the event dictionary.
75
+
76
+ Uvicorn logs the message a second time in the extra `color_message`, but we don't
77
+ need it. This processor drops the key from the event dict if it exists.
78
+ """
79
+ event_dict.pop("color_message", None)
80
+ return event_dict
81
+
82
+
83
+ def setup_log(
84
+ mode: LogModeEnum = LogModeEnum.CONSOLE, log_level: str = "DEBUG", logging_config: list[LoggingConfig] | None = None
85
+ ) -> None:
86
+ """Prepares the logging configuration.
87
+
88
+ Args:
89
+ mode (LogMode): The logging mode to use.
90
+ log_level (str): The log level to use.
91
+ logging_config (List[LoggingConfig], optional): The logging configuration. Defaults to None.
92
+
93
+ Returns:
94
+ None
95
+ """
96
+ processors: list[structlog.typing.Processor] = [
97
+ structlog.stdlib.add_logger_name,
98
+ structlog.stdlib.add_log_level,
99
+ structlog.processors.TimeStamper(fmt="iso"),
100
+ structlog.processors.UnicodeDecoder(),
101
+ structlog.stdlib.PositionalArgumentsFormatter(),
102
+ structlog.stdlib.ExtraAdder(),
103
+ structlog.processors.CallsiteParameterAdder(
104
+ parameters={
105
+ structlog.processors.CallsiteParameter.MODULE: True,
106
+ structlog.processors.CallsiteParameter.FUNC_NAME: True,
107
+ structlog.processors.CallsiteParameter.LINENO: True,
108
+ }
109
+ ),
110
+ _drop_color_message_key,
111
+ ]
112
+
113
+ log_renderer: structlog.dev.ConsoleRenderer | structlog.processors.JSONRenderer
114
+ match mode:
115
+ case LogModeEnum.CONSOLE:
116
+ log_renderer = structlog.dev.ConsoleRenderer(
117
+ exception_formatter=structlog.dev.RichTracebackFormatter(),
118
+ )
119
+ case LogModeEnum.JSON:
120
+ # We rename the `event` key to `message` only in JSON logs,
121
+ # as Datadog looks for the
122
+ # `message` key but the pretty ConsoleRenderer looks for `event`
123
+ processors.append(_rename_event_key)
124
+ # Format the exception only for JSON logs, as we want
125
+ # to pretty-print them when
126
+ # using the ConsoleRenderer
127
+ processors.append(
128
+ structlog.processors.dict_tracebacks,
129
+ )
130
+ log_renderer = structlog.processors.JSONRenderer()
131
+
132
+ # Remove all existing loggers
133
+ structlog.reset_defaults()
134
+ structlog_processors: list[structlog.typing.Processor] = processors.copy()
135
+ structlog_processors.append(structlog.stdlib.ProcessorFormatter.wrap_for_formatter)
136
+ structlog.configure(
137
+ processors=structlog_processors,
138
+ logger_factory=structlog.stdlib.LoggerFactory(),
139
+ cache_logger_on_first_use=True,
140
+ )
141
+
142
+ formatter = structlog.stdlib.ProcessorFormatter(
143
+ # These run ONLY on `logging` entries that do NOT originate within
144
+ # structlog.
145
+ foreign_pre_chain=processors,
146
+ # These run on ALL entries after the pre_chain is done.
147
+ processors=[
148
+ # Remove _record & _from_structlog.
149
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
150
+ log_renderer,
151
+ ],
152
+ )
153
+
154
+ handler = logging.StreamHandler()
155
+ # Use OUR `ProcessorFormatter` to format all `logging` entries.
156
+ handler.setFormatter(formatter)
157
+ root_logger: logging.Logger = logging.getLogger()
158
+ root_logger.addHandler(handler)
159
+ root_logger.setLevel(log_level.upper())
160
+
161
+ for logging_config_item in logging_config or []:
162
+ logger = logging.getLogger(logging_config_item.name)
163
+ logger.setLevel(logging_config_item.level)
164
+
165
+ def handle_exception(exc_type: Any, exc_value: Any, exc_traceback: Any) -> None:
166
+ """Handle uncaught exceptions.
167
+
168
+ Log any uncaught exception instead of letting it be printed by Python
169
+ (but leave KeyboardInterrupt untouched to allow users to Ctrl+C to stop)
170
+ See https://stackoverflow.com/a/16993115/3641865
171
+ """
172
+ if issubclass(exc_type, KeyboardInterrupt):
173
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
174
+ return
175
+
176
+ _logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
177
+
178
+ sys.excepthook = handle_exception
@@ -0,0 +1,45 @@
1
+ """Provides utilities for the application."""
2
+
3
+ import uvicorn
4
+ import uvicorn.server
5
+
6
+ from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
7
+ from fastapi_factory_utilities.core.utils.log import clean_uvicorn_logger
8
+
9
+
10
+ class UvicornUtils:
11
+ """Provides utilities for Uvicorn."""
12
+
13
+ def __init__(self, app: BaseApplicationProtocol) -> None:
14
+ """Instantiate the factory.
15
+
16
+ Args:
17
+ app (BaseApplication): The application.
18
+ config (AppConfigAbstract): The application configuration.
19
+
20
+ Returns:
21
+ None
22
+ """
23
+ self._app: BaseApplicationProtocol = app
24
+
25
+ def build_uvicorn_config(self) -> uvicorn.Config:
26
+ """Build the Uvicorn configuration.
27
+
28
+ Returns:
29
+ uvicorn.Config: The Uvicorn configuration.
30
+ """
31
+ config = uvicorn.Config(
32
+ app=self._app.get_asgi_app(),
33
+ host=self._app.get_config().host,
34
+ port=self._app.get_config().port,
35
+ reload=self._app.get_config().reload,
36
+ workers=self._app.get_config().workers,
37
+ )
38
+ clean_uvicorn_logger()
39
+ return config
40
+
41
+ def serve(self) -> None:
42
+ """Serve the application."""
43
+ config: uvicorn.Config = self.build_uvicorn_config()
44
+ server: uvicorn.Server = uvicorn.Server(config=config)
45
+ server.run()
@@ -0,0 +1,166 @@
1
+ """Provides a class for reading YAML files and converting them to Pydantic models."""
2
+
3
+ # mypy: disable-error-code="unused-ignore"
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ from structlog.stdlib import BoundLogger, get_logger
11
+ from yaml import SafeLoader
12
+
13
+ logger: BoundLogger = get_logger()
14
+
15
+
16
+ class UnableToReadYamlFileError(Exception):
17
+ """Raised when there is an error reading a YAML file."""
18
+
19
+ def __init__(self, file_path: Path | None = None, message: str = "") -> None:
20
+ """Initializes the exception.
21
+
22
+ Args:
23
+ file_path (str): The path to the YAML file.
24
+ message (str): The error message.
25
+ """
26
+ super().__init__(f"Error reading YAML file: {file_path} - {message}")
27
+
28
+
29
+ class YamlFileReader:
30
+ """Handles reading YAML files and converting them to Pydantic models."""
31
+
32
+ re_pattern: re.Pattern[str] = re.compile(r"\${([A-Za-z0-9\-\_]+):?([A-Za-z0-9\-\_\/\:]*)?}")
33
+
34
+ def __init__(
35
+ self,
36
+ file_path: Path,
37
+ yaml_base_key: str | None = None,
38
+ use_environment_injection: bool = True,
39
+ ) -> None:
40
+ """Initializes the YAML file reader.
41
+
42
+ Args:
43
+ file_path (str): The path to the YAML file.
44
+ yaml_base_key (str | None, optional): The base key
45
+ in the YAML file to read from. Defaults to None.
46
+ use_environment_injection (bool, optional): Whether to use
47
+ environment injection. Defaults to True.
48
+ """
49
+ # Store the file path and base key for YAML reading
50
+ self._yaml_base_key: str | None = yaml_base_key
51
+ self._file_path: Path = file_path
52
+
53
+ # Store whether to use environment injection
54
+ self._use_environment_injection: bool = use_environment_injection
55
+
56
+ def _filter_data_with_base_key(self, yaml_data: dict[str, Any]) -> dict[str, Any] | None:
57
+ """Extracts the data from the YAML file with the base key.
58
+
59
+ Args:
60
+ yaml_data (dict): The data from the YAML file.
61
+
62
+ Returns:
63
+ dict: The filtered data from the YAML file.
64
+
65
+ Raises:
66
+ KeyError: If the base key is not found in the YAML file.
67
+ """
68
+ if self._yaml_base_key is not None:
69
+ keys: list[str] = self._yaml_base_key.split(".")
70
+ while len(keys) != 0:
71
+ key: str = keys.pop(0)
72
+ try:
73
+ yaml_data = yaml_data[key]
74
+ except KeyError:
75
+ logger.warning(f"Base key {key}" " not found in YAML file" + " from {self._yaml_base_key}")
76
+ return dict()
77
+ return yaml_data
78
+
79
+ def _read_yaml_file(self, file_path: Path) -> dict[str, Any]:
80
+ """Reads the YAML file and returns the data as a dictionary.
81
+
82
+ Args:
83
+ file_path (Path): The path to the YAML file.
84
+
85
+ Returns:
86
+ dict: The data from the YAML file.
87
+
88
+ Raises:
89
+ ValueError: If there is an error reading the file.
90
+ FileNotFoundError: If the file is not found.
91
+ """
92
+ if not os.path.exists(file_path):
93
+ raise FileNotFoundError(f"File not found: {file_path}")
94
+
95
+ with open(file=file_path, encoding="UTF-8") as file:
96
+ loader = SafeLoader(file)
97
+
98
+ try:
99
+ yaml_data: dict[str, Any] = cast(
100
+ dict[str, Any],
101
+ loader.get_data(), # type: ignore
102
+ )
103
+ except Exception as exception:
104
+ raise ValueError(f"Error reading YAML file: {file_path}") from exception
105
+
106
+ return yaml_data
107
+
108
+ def _inject_environment_variables(
109
+ self, yaml_data: dict[str, Any] | str | list[str] | bool | int
110
+ ) -> dict[str, Any] | str | list[str] | bool | int:
111
+ """Injects environment variables into the YAML data recursively.
112
+
113
+ Args:
114
+ yaml_data (dict | str | list | bool): The data from the YAML file.
115
+
116
+ Returns:
117
+ dict: The data from the YAML file
118
+ with environment variables injected.
119
+
120
+ Raises:
121
+ ValueError: If the YAML data is None.
122
+ """
123
+ if isinstance(yaml_data, dict):
124
+ for key, value in yaml_data.items():
125
+ yaml_data[key] = self._inject_environment_variables(value)
126
+ elif isinstance(yaml_data, list):
127
+ yaml_data = [cast(str, self._inject_environment_variables(yaml_data=value)) for value in yaml_data]
128
+ elif isinstance(yaml_data, bool) or isinstance(yaml_data, int):
129
+ return yaml_data
130
+ elif isinstance(yaml_data, str): # type: ignore
131
+ while True:
132
+ match = self.re_pattern.search(yaml_data)
133
+ if match is None:
134
+ break
135
+ env_key = match.group(1)
136
+ env_default = match.group(2)
137
+ env_value = os.getenv(env_key, env_default)
138
+ yaml_data = yaml_data.replace(match.group(0), env_value)
139
+ else:
140
+ raise ValueError(f"Type not supported: {type(yaml_data)}")
141
+ return yaml_data
142
+
143
+ def read(self) -> dict[str, Any]:
144
+ """Reads the YAML file and converts it to a Pydantic model with env injected.
145
+
146
+ Raises:
147
+ UnableToReadYamlFileError: If there is an error reading the file.
148
+ """
149
+ # Read the YAML file and filter the data with the base key
150
+ try:
151
+ yaml_data: dict[str, Any] | None = self._filter_data_with_base_key(
152
+ self._read_yaml_file(file_path=self._file_path)
153
+ )
154
+ except (FileNotFoundError, ValueError, KeyError) as exception:
155
+ raise UnableToReadYamlFileError(file_path=self._file_path, message=str(exception)) from exception
156
+
157
+ if yaml_data is None:
158
+ return dict()
159
+
160
+ if self._use_environment_injection:
161
+ yaml_data_with_env_injected: dict[str, Any] = cast(
162
+ dict[str, Any], self._inject_environment_variables(yaml_data)
163
+ )
164
+ return dict[str, Any](yaml_data_with_env_injected)
165
+ else:
166
+ return dict[str, Any](yaml_data)
@@ -0,0 +1,11 @@
1
+ """Python Factory Example."""
2
+
3
+ from fastapi_factory_utilities.example.app import App
4
+
5
+
6
+ def main() -> None:
7
+ """Main function."""
8
+ App.main()
9
+
10
+
11
+ __all__: list[str] = ["App", "main"]
@@ -0,0 +1,6 @@
1
+ """Main entry point for the application."""
2
+
3
+ from fastapi_factory_utilities.example import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,19 @@
1
+ """Provide the API for the example package."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from .books import api_v1_books_router, api_v2_books_router
6
+
7
+ api_router: APIRouter = APIRouter(prefix="/api")
8
+
9
+ # -- API v1
10
+ api_router_v1: APIRouter = APIRouter(prefix="/v1")
11
+ api_router_v1.include_router(router=api_v1_books_router)
12
+
13
+ # -- API v2
14
+ api_router_v2: APIRouter = APIRouter(prefix="/v2")
15
+ api_router_v2.include_router(router=api_v2_books_router)
16
+
17
+ # -- Include API versions
18
+ api_router.include_router(router=api_router_v1)
19
+ api_router.include_router(router=api_router_v2)
@@ -0,0 +1,5 @@
1
+ """Package for books API."""
2
+
3
+ from .routes import api_v1_books_router, api_v2_books_router
4
+
5
+ __all__: list[str] = ["api_v1_books_router", "api_v2_books_router"]
@@ -0,0 +1,26 @@
1
+ """Provides the response objects for the books API."""
2
+
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from fastapi_factory_utilities.example.entities.books import BookName, BookType
8
+
9
+
10
+ class BookResponseModel(BaseModel):
11
+ """Book response model."""
12
+
13
+ model_config = ConfigDict(extra="ignore")
14
+
15
+ id: UUID
16
+ title: BookName
17
+ book_type: BookType
18
+
19
+
20
+ class BookListReponse(BaseModel):
21
+ """Book list response."""
22
+
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ books: list[BookResponseModel]
26
+ size: int
@@ -0,0 +1,62 @@
1
+ """Provides the Books API."""
2
+
3
+ from typing import cast
4
+ from uuid import UUID
5
+
6
+ from fastapi import APIRouter, Depends, Request
7
+
8
+ from fastapi_factory_utilities.example.entities.books import BookEntity
9
+ from fastapi_factory_utilities.example.models.books.repository import BookRepository
10
+ from fastapi_factory_utilities.example.services.books import BookService
11
+
12
+ from .responses import BookListReponse, BookResponseModel
13
+
14
+ api_v1_books_router: APIRouter = APIRouter(prefix="/books")
15
+ api_v2_books_router: APIRouter = APIRouter(prefix="/books")
16
+
17
+
18
+ def get_book_service(request: Request) -> BookService:
19
+ """Provide Book Service."""
20
+ return BookService(book_repository=BookRepository(request.app.state.odm_client))
21
+
22
+
23
+ @api_v1_books_router.get(path="", response_model=BookListReponse)
24
+ def get_books(
25
+ books_service: BookService = Depends(get_book_service),
26
+ ) -> BookListReponse:
27
+ """Get all books.
28
+
29
+ Args:
30
+ books_service (BookService): Book service.
31
+
32
+ Returns:
33
+ BookListReponse: List of books
34
+ """
35
+ books: list[BookEntity] = books_service.get_all_books()
36
+
37
+ return BookListReponse(
38
+ books=cast(
39
+ list[BookResponseModel],
40
+ map(lambda book: BookResponseModel(**book.model_dump()), books),
41
+ ),
42
+ size=len(books),
43
+ )
44
+
45
+
46
+ @api_v1_books_router.get(path="/{book_id}", response_model=BookResponseModel)
47
+ def get_book(
48
+ book_id: UUID,
49
+ books_service: BookService = Depends(get_book_service),
50
+ ) -> BookResponseModel:
51
+ """Get a book.
52
+
53
+ Args:
54
+ book_id (str): Book id
55
+ books_service (BookService): Book service
56
+
57
+ Returns:
58
+ BookResponseModel: Book
59
+ """
60
+ book: BookEntity = books_service.get_book(book_id)
61
+
62
+ return BookResponseModel(**book.model_dump())
@@ -0,0 +1,6 @@
1
+ """Provides the App and AppConfig classes."""
2
+
3
+ from .app import App
4
+ from .config import AppConfig
5
+
6
+ __all__: list[str] = ["App", "AppConfig"]
@@ -0,0 +1,37 @@
1
+ """Provides the concrete application class."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from beanie import Document
6
+
7
+ from fastapi_factory_utilities.core.app import BaseApplication
8
+ from fastapi_factory_utilities.core.app.base.plugins_manager_abstract import (
9
+ PluginsActivationList,
10
+ )
11
+ from fastapi_factory_utilities.example.models.books.document import BookDocument
12
+
13
+ from .config import AppConfig
14
+
15
+
16
+ class App(BaseApplication):
17
+ """Concrete application class."""
18
+
19
+ PACKAGE_NAME: str = "fastapi_factory_utilities.example"
20
+
21
+ CONFIG_CLASS = AppConfig
22
+
23
+ ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]] = [BookDocument]
24
+
25
+ def __init__(self, config: AppConfig, plugin_activation_list: PluginsActivationList | None = None) -> None:
26
+ """Instantiate the application with the configuration and the API router.
27
+
28
+ Args:
29
+ config (AppConfig): The application configuration.
30
+ plugin_activation_list (PluginsActivationList | None, optional): The plugins activation list.
31
+ """
32
+ super().__init__(config=config, plugin_activation_list=plugin_activation_list)
33
+
34
+ # Prevent circular imports
35
+ from ..api import api_router # pylint: disable=import-outside-toplevel
36
+
37
+ self.get_asgi_app().include_router(router=api_router)