oe-python-template 0.9.7__py3-none-any.whl → 0.10.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.
Files changed (37) hide show
  1. oe_python_template/__init__.py +3 -17
  2. oe_python_template/api.py +42 -148
  3. oe_python_template/cli.py +13 -141
  4. oe_python_template/constants.py +6 -9
  5. oe_python_template/hello/__init__.py +17 -0
  6. oe_python_template/hello/_api.py +94 -0
  7. oe_python_template/hello/_cli.py +47 -0
  8. oe_python_template/hello/_constants.py +4 -0
  9. oe_python_template/hello/_models.py +28 -0
  10. oe_python_template/hello/_service.py +96 -0
  11. oe_python_template/{settings.py → hello/_settings.py} +6 -4
  12. oe_python_template/system/__init__.py +17 -0
  13. oe_python_template/system/_api.py +78 -0
  14. oe_python_template/system/_cli.py +165 -0
  15. oe_python_template/system/_service.py +163 -0
  16. oe_python_template/utils/__init__.py +57 -0
  17. oe_python_template/utils/_api.py +18 -0
  18. oe_python_template/utils/_cli.py +68 -0
  19. oe_python_template/utils/_console.py +14 -0
  20. oe_python_template/utils/_constants.py +48 -0
  21. oe_python_template/utils/_di.py +70 -0
  22. oe_python_template/utils/_health.py +107 -0
  23. oe_python_template/utils/_log.py +122 -0
  24. oe_python_template/utils/_logfire.py +67 -0
  25. oe_python_template/utils/_process.py +41 -0
  26. oe_python_template/utils/_sentry.py +96 -0
  27. oe_python_template/utils/_service.py +39 -0
  28. oe_python_template/utils/_settings.py +68 -0
  29. oe_python_template/utils/boot.py +86 -0
  30. {oe_python_template-0.9.7.dist-info → oe_python_template-0.10.0.dist-info}/METADATA +77 -51
  31. oe_python_template-0.10.0.dist-info/RECORD +34 -0
  32. oe_python_template/models.py +0 -44
  33. oe_python_template/service.py +0 -68
  34. oe_python_template-0.9.7.dist-info/RECORD +0 -12
  35. {oe_python_template-0.9.7.dist-info → oe_python_template-0.10.0.dist-info}/WHEEL +0 -0
  36. {oe_python_template-0.9.7.dist-info → oe_python_template-0.10.0.dist-info}/entry_points.txt +0 -0
  37. {oe_python_template-0.9.7.dist-info → oe_python_template-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,14 @@
1
+ """Themed rich console."""
2
+
3
+ from rich.console import Console
4
+ from rich.theme import Theme
5
+
6
+ console = Console(
7
+ theme=Theme({
8
+ "logging.level.info": "purple4",
9
+ "debug": "light_cyan3",
10
+ "info": "purple4",
11
+ "warning": "yellow1",
12
+ "error": "red1",
13
+ }),
14
+ )
@@ -0,0 +1,48 @@
1
+ """Constants used throughout."""
2
+
3
+ import os
4
+ import sys
5
+ from importlib import metadata
6
+ from pathlib import Path
7
+
8
+ __project_name__ = __name__.split(".")[0]
9
+ __project_path__ = str(Path(__file__).parent.parent.parent)
10
+ __version__ = metadata.version(__project_name__)
11
+ __is_development_mode__ = "uvx" not in sys.argv[0].lower()
12
+ __is_running_in_container__ = os.getenv(f"{__project_name__.upper()}_RUNNING_IN_CONTAINER")
13
+ __env__ = os.getenv("ENV", "local")
14
+ __env_file__ = [
15
+ Path.home() / f".{__project_name__}" / ".env",
16
+ Path.home() / f".{__project_name__}" / f".env.{__env__}",
17
+ Path(".env"),
18
+ Path(f".env.{__env__}"),
19
+ ]
20
+ env_file_path = os.getenv(f"{__project_name__.upper()}_ENV_FILE")
21
+ if env_file_path:
22
+ __env_file__.insert(2, Path(env_file_path))
23
+
24
+
25
+ def get_project_url_by_label(prefix: str) -> str:
26
+ """Get labeled Project-URL.
27
+
28
+ See https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata-project-url
29
+
30
+ Args:
31
+ prefix(str): The prefix to match at the beginning of URL entries.
32
+
33
+ Returns:
34
+ The extracted URL string if found, or an empty string if not found.
35
+ """
36
+ for url_entry in metadata.metadata(__project_name__).get_all("Project-URL", []):
37
+ if url_entry.startswith(prefix):
38
+ return str(url_entry.split(", ", 1)[1])
39
+
40
+ return ""
41
+
42
+
43
+ _authors = metadata.metadata(__project_name__).get_all("Author-email", [])
44
+ _author = _authors[0] if _authors else None
45
+ __author_name__ = _author.split("<")[0].strip() if _author else None
46
+ __author_email__ = _author.split("<")[1].strip(" >") if _author else None
47
+ __repository_url__ = get_project_url_by_label("Source")
48
+ __documentation__url__ = get_project_url_by_label("Documentation")
@@ -0,0 +1,70 @@
1
+ """Module for dynamic import and discovery of implementations and subclasses."""
2
+
3
+ import importlib
4
+ import pkgutil
5
+ from inspect import isclass
6
+ from typing import Any
7
+
8
+ from ._constants import __project_name__
9
+
10
+ _implementation_cache: dict[Any, list[Any]] = {}
11
+ _subclass_cache: dict[Any, list[Any]] = {}
12
+
13
+
14
+ def locate_implementations(_class: type[Any]) -> list[Any]:
15
+ """
16
+ Dynamically discover all instances of some class.
17
+
18
+ Args:
19
+ _class (type[Any]): Class to search for.
20
+
21
+ Returns:
22
+ list[Any]: List of discovered implementations of the given class.
23
+ """
24
+ if _class in _implementation_cache:
25
+ return _implementation_cache[_class]
26
+
27
+ implementations = []
28
+ package = importlib.import_module(__project_name__)
29
+
30
+ for _, name, _ in pkgutil.iter_modules(package.__path__):
31
+ module = importlib.import_module(f"{__project_name__}.{name}")
32
+ # Check all members of the module
33
+ for member_name in dir(module):
34
+ member = getattr(module, member_name)
35
+ if isinstance(member, _class):
36
+ implementations.append(member)
37
+
38
+ _implementation_cache[_class] = implementations
39
+ return implementations
40
+
41
+
42
+ def locate_subclasses(_class: type[Any]) -> list[Any]:
43
+ """
44
+ Dynamically discover all classes that are subclasses of some type.
45
+
46
+ Args:
47
+ _class (type[Any]): Parent class of subclasses to search for.
48
+
49
+ Returns:
50
+ list[type[Any]]: List of discovered subclasses of the given class.
51
+ """
52
+ if _class in _subclass_cache:
53
+ return _subclass_cache[_class]
54
+
55
+ subclasses = []
56
+ package = importlib.import_module(__project_name__)
57
+
58
+ for _, name, _ in pkgutil.iter_modules(package.__path__):
59
+ try:
60
+ module = importlib.import_module(f"{__project_name__}.{name}")
61
+ # Check all members of the module
62
+ for member_name in dir(module):
63
+ member = getattr(module, member_name)
64
+ if isclass(member) and issubclass(member, _class) and member != _class:
65
+ subclasses.append(member)
66
+ except ImportError:
67
+ continue
68
+
69
+ _subclass_cache[_class] = subclasses
70
+ return subclasses
@@ -0,0 +1,107 @@
1
+ """Health models and status definitions for service health checks."""
2
+
3
+ from enum import StrEnum
4
+ from typing import ClassVar, Self
5
+
6
+ from pydantic import BaseModel, Field, model_validator
7
+
8
+
9
+ class _HealthStatus(StrEnum):
10
+ UP = "UP"
11
+ DOWN = "DOWN"
12
+
13
+
14
+ class Health(BaseModel):
15
+ """Represents the health status of a service with optional components and failure reasons.
16
+
17
+ - A health object can have child components, i.e. health forms a tree.
18
+ - Any node in the tree can set itself to DOWN. In this case the node is required
19
+ to set the reason attribute. If reason is not set when DOWN,
20
+ automatic model validation of the tree will fail.
21
+ - DOWN'ness is propagated to parent health objects. I.e. the health of a parent
22
+ node is automatically set to DOWN if any of its child components are DOWN. The
23
+ child components leading to this will be listed in the reason.
24
+ - The root of the health tree is computed in the system module. The health of other
25
+ modules is automatically picked up by the system module.
26
+ """
27
+
28
+ Code: ClassVar[type[_HealthStatus]] = _HealthStatus
29
+ status: _HealthStatus
30
+ reason: str | None = None
31
+ components: dict[str, "Health"] = Field(default_factory=dict)
32
+
33
+ def compute_health_from_components(self) -> Self:
34
+ """Recursively compute health status from components.
35
+
36
+ - If health is already DOWN, it remains DOWN with its original reason.
37
+ - If health is UP but any component is DOWN, health becomes DOWN with
38
+ a reason listing all failed components.
39
+
40
+ Returns:
41
+ Self: The updated health instance with computed status.
42
+ """
43
+ # Skip recomputation if already known to be DOWN
44
+ if self.status == _HealthStatus.DOWN:
45
+ return self
46
+
47
+ # No components means we keep the existing status
48
+ if not self.components:
49
+ return self
50
+
51
+ # Find all DOWN components
52
+ down_components = []
53
+ for component_name, component in self.components.items():
54
+ # Recursively compute health for each component
55
+ component.compute_health_from_components()
56
+ if component.status == _HealthStatus.DOWN:
57
+ down_components.append(component_name)
58
+
59
+ # If any components are DOWN, mark the parent as DOWN
60
+ if down_components:
61
+ self.status = _HealthStatus.DOWN
62
+ if len(down_components) == 1:
63
+ self.reason = f"Component '{down_components[0]}' is DOWN"
64
+ else:
65
+ component_list = "', '".join(down_components)
66
+ self.reason = f"Components '{component_list}' are DOWN"
67
+
68
+ return self
69
+
70
+ @model_validator(mode="after")
71
+ def validate_health_state(self) -> Self:
72
+ """Validate the health state and ensure consistency.
73
+
74
+ - Compute overall health based on component health
75
+ - Ensure UP status has no associated reason
76
+ - Ensure DOWN status always has a reason
77
+
78
+ Returns:
79
+ Self: The validated model instance with correct health status.
80
+
81
+ Raises:
82
+ ValueError: If validation fails due to inconsistency.
83
+ """
84
+ # First compute health from components
85
+ self.compute_health_from_components()
86
+
87
+ # Validate that UP status has no reason
88
+ if (self.status == _HealthStatus.UP) and self.reason:
89
+ msg = f"Health {self.status} must not have reason"
90
+ raise ValueError(msg)
91
+
92
+ # Validate that DOWN status always has a reason
93
+ if (self.status == _HealthStatus.DOWN) and not self.reason:
94
+ msg = "Health DOWN must have a reason"
95
+ raise ValueError(msg)
96
+
97
+ return self
98
+
99
+ def __str__(self) -> str:
100
+ """Return string representation of health status with optional reason for DOWN state.
101
+
102
+ Returns:
103
+ str: The health status value, with reason appended if status is DOWN.
104
+ """
105
+ if self.status == _HealthStatus.DOWN and self.reason:
106
+ return f"{self.status.value}: {self.reason}"
107
+ return self.status.value
@@ -0,0 +1,122 @@
1
+ """Logging configuration and utilities."""
2
+
3
+ import logging as python_logging
4
+ import typing as t
5
+ from logging import FileHandler
6
+ from typing import Annotated, Literal
7
+
8
+ import click
9
+ import logfire
10
+ from pydantic import Field
11
+ from pydantic_settings import BaseSettings, SettingsConfigDict
12
+ from rich.console import Console
13
+ from rich.logging import RichHandler
14
+
15
+ from ._constants import __env_file__, __project_name__
16
+ from ._settings import load_settings
17
+
18
+
19
+ def get_logger(name: str | None) -> python_logging.Logger:
20
+ """
21
+ Get a logger instance with the given name or project name as default.
22
+
23
+ Args:
24
+ name(str): The name for the logger. If None, uses project name.
25
+
26
+ Returns:
27
+ Logger: Configured logger instance.
28
+ """
29
+ if (name is None) or (name == __project_name__):
30
+ return python_logging.getLogger(__project_name__)
31
+ return python_logging.getLogger(f"{__project_name__}.{name}")
32
+
33
+
34
+ class LogSettings(BaseSettings):
35
+ """Settings for configuring logging behavior."""
36
+
37
+ model_config = SettingsConfigDict(
38
+ env_prefix=f"{__project_name__.upper()}_LOG_",
39
+ extra="ignore",
40
+ env_file=__env_file__,
41
+ env_file_encoding="utf-8",
42
+ )
43
+
44
+ level: Annotated[
45
+ Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
46
+ Field(description="Logging level", default="INFO"),
47
+ ]
48
+ file_enabled: Annotated[
49
+ bool,
50
+ Field(description="Enable logging to file", default=False),
51
+ ]
52
+ file_name: Annotated[
53
+ str,
54
+ Field(description="Name of the log file", default=f"{__project_name__}.log"),
55
+ ]
56
+ console_enabled: Annotated[
57
+ bool,
58
+ Field(description="Enable logging to console", default=False),
59
+ ]
60
+
61
+
62
+ class CustomFilter(python_logging.Filter):
63
+ """Custom filter for log records."""
64
+
65
+ @staticmethod
66
+ def filter(_record: python_logging.LogRecord) -> bool:
67
+ """
68
+ Filter log records based on custom criteria.
69
+
70
+ Args:
71
+ record: The log record to filter.
72
+
73
+ Returns:
74
+ bool: True if record should be logged, False otherwise.
75
+ """
76
+ return True
77
+
78
+
79
+ def logging_initialize(log_to_logfire: bool = False) -> None:
80
+ """Initialize logging configuration."""
81
+ log_filter = CustomFilter()
82
+
83
+ handlers = []
84
+
85
+ settings = load_settings(LogSettings)
86
+
87
+ if settings.file_enabled:
88
+ file_handler = python_logging.FileHandler(settings.file_name)
89
+ file_formatter = python_logging.Formatter(
90
+ fmt="%(asctime)s %(process)d %(levelname)s %(name)s %(message)s",
91
+ datefmt="%Y-%m-%d %H:%M:%S",
92
+ )
93
+ file_handler.setFormatter(file_formatter)
94
+ file_handler.addFilter(log_filter)
95
+ handlers.append(file_handler)
96
+
97
+ if settings.console_enabled:
98
+ rich_handler = RichHandler(
99
+ console=Console(stderr=True),
100
+ markup=True,
101
+ rich_tracebacks=True,
102
+ tracebacks_suppress=[click],
103
+ show_time=True,
104
+ omit_repeated_times=True,
105
+ show_path=True,
106
+ show_level=True,
107
+ enable_link_path=True,
108
+ )
109
+ rich_handler.addFilter(log_filter)
110
+ handlers.append(t.cast("FileHandler", rich_handler))
111
+
112
+ if log_to_logfire:
113
+ logfire_handler = logfire.LogfireLoggingHandler()
114
+ logfire_handler.addFilter(log_filter)
115
+ handlers.append(t.cast("FileHandler", logfire_handler))
116
+
117
+ python_logging.basicConfig(
118
+ level=settings.level,
119
+ format="%(name)s %(message)s",
120
+ datefmt="%Y-%m-%d %H:%M:%S",
121
+ handlers=handlers,
122
+ )
@@ -0,0 +1,67 @@
1
+ """Logfire integration for logging and instrumentation."""
2
+
3
+ from typing import Annotated
4
+
5
+ import logfire
6
+ from pydantic import Field, SecretStr
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+ from ._constants import __env__, __env_file__, __project_name__, __repository_url__, __version__
10
+ from ._settings import load_settings
11
+
12
+
13
+ class LogfireSettings(BaseSettings):
14
+ """Configuration settings for Logfire integration."""
15
+
16
+ model_config = SettingsConfigDict(
17
+ env_prefix=f"{__project_name__.upper()}_LOGFIRE_",
18
+ env_file=__env_file__,
19
+ env_file_encoding="utf-8",
20
+ extra="ignore",
21
+ )
22
+
23
+ token: Annotated[
24
+ SecretStr | None,
25
+ Field(description="Logfire token. Leave empty to disable logfire.", examples=["YOUR_TOKEN"], default=None),
26
+ ]
27
+ instrument_system_metrics: Annotated[
28
+ bool,
29
+ Field(description="Enable system metrics instrumentation", default=False),
30
+ ]
31
+
32
+
33
+ def logfire_initialize(modules: list["str"]) -> bool:
34
+ """Initialize Logfire integration.
35
+
36
+ Args:
37
+ modules(list["str"]): List of modules to be instrumented.
38
+
39
+ Returns:
40
+ bool: True if initialized successfully False otherwise
41
+ """
42
+ settings = load_settings(LogfireSettings)
43
+
44
+ if settings.token is None:
45
+ return False
46
+
47
+ logfire.configure(
48
+ send_to_logfire="if-token-present",
49
+ token=settings.token.get_secret_value(),
50
+ environment=__env__,
51
+ service_name=__project_name__,
52
+ console=False,
53
+ code_source=logfire.CodeSource(
54
+ repository=__repository_url__,
55
+ revision=__version__,
56
+ root_path="",
57
+ ),
58
+ )
59
+
60
+ if settings.instrument_system_metrics:
61
+ logfire.instrument_system_metrics(base="full")
62
+
63
+ logfire.instrument_pydantic()
64
+
65
+ logfire.install_auto_tracing(modules=modules, min_duration=0.0)
66
+
67
+ return True
@@ -0,0 +1,41 @@
1
+ """Process related utilities."""
2
+
3
+ from pathlib import Path
4
+
5
+ import psutil
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class ParentProcessInfo(BaseModel):
10
+ """Information about a parent process."""
11
+
12
+ name: str | None = None
13
+ pid: int | None = None
14
+
15
+
16
+ class ProcessInfo(BaseModel):
17
+ """Information about the current process."""
18
+
19
+ project_root: str
20
+ pid: int
21
+ parent: ParentProcessInfo
22
+
23
+
24
+ def get_process_info() -> ProcessInfo:
25
+ """
26
+ Get information about the current process and its parent.
27
+
28
+ Returns:
29
+ ProcessInfo: Object containing process information.
30
+ """
31
+ current_process = psutil.Process()
32
+ parent = current_process.parent()
33
+
34
+ return ProcessInfo(
35
+ project_root=str(Path(__file__).parent.parent.parent.parent),
36
+ pid=current_process.pid,
37
+ parent=ParentProcessInfo(
38
+ name=parent.name() if parent else None,
39
+ pid=parent.pid if parent else None,
40
+ ),
41
+ )
@@ -0,0 +1,96 @@
1
+ """Sentry integration for application monitoring."""
2
+
3
+ from typing import Annotated
4
+
5
+ import sentry_sdk
6
+ from pydantic import Field, SecretStr
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+ from sentry_sdk.integrations.typer import TyperIntegration
9
+
10
+ from ._constants import __env__, __env_file__, __project_name__, __version__
11
+ from ._settings import load_settings
12
+
13
+
14
+ class SentrySettings(BaseSettings):
15
+ """Configuration settings for Sentry integration."""
16
+
17
+ model_config = SettingsConfigDict(
18
+ env_prefix=f"{__project_name__.upper()}_SENTRY_",
19
+ env_file=__env_file__,
20
+ env_file_encoding="utf-8",
21
+ extra="ignore",
22
+ )
23
+
24
+ dsn: Annotated[
25
+ SecretStr | None,
26
+ Field(description="Sentry DSN", examples=["https://SECRET@SECRET.ingest.de.sentry.io/SECRET"], default=None),
27
+ ]
28
+
29
+ debug: Annotated[
30
+ bool,
31
+ Field(description="Debug (https://docs.sentry.io/platforms/python/configuration/options/)", default=False),
32
+ ]
33
+
34
+ send_default_pii: Annotated[
35
+ bool,
36
+ Field(
37
+ description="Send default personal identifiable information (https://docs.sentry.io/platforms/python/configuration/options/)",
38
+ default=False,
39
+ ),
40
+ ]
41
+
42
+ max_breadcrumbs: Annotated[
43
+ int,
44
+ Field(
45
+ description="Max breadcrumbs (https://docs.sentry.io/platforms/python/configuration/options/#max_breadcrumbs)",
46
+ default=5.0,
47
+ ),
48
+ ]
49
+ sample_rate: Annotated[
50
+ float,
51
+ Field(
52
+ description="Sample Rate (https://docs.sentry.io/platforms/python/configuration/sampling/#sampling-error-events)",
53
+ default=1.0,
54
+ ),
55
+ ]
56
+ traces_sample_rate: Annotated[
57
+ float,
58
+ Field(
59
+ description="Traces Sample Rate (https://docs.sentry.io/platforms/python/configuration/sampling/#configuring-the-transaction-sample-rate)",
60
+ default=1.0,
61
+ ),
62
+ ]
63
+ profiles_sample_rate: Annotated[
64
+ float,
65
+ Field(
66
+ description="Traces Sample Rate (https://docs.sentry.io/platforms/python/tracing/#configure)",
67
+ default=1.0,
68
+ ),
69
+ ]
70
+
71
+
72
+ def sentry_initialize() -> bool:
73
+ """Initialize Sentry integration.
74
+
75
+ Returns:
76
+ bool: True if initialized successfully, False otherwise
77
+ """
78
+ settings = load_settings(SentrySettings)
79
+
80
+ if settings.dsn is None:
81
+ return False
82
+
83
+ sentry_sdk.init(
84
+ release=f"{__project_name__}@{__version__}", # https://docs.sentry.io/platforms/python/configuration/releases/,
85
+ environment=__env__,
86
+ dsn=settings.dsn.get_secret_value(),
87
+ max_breadcrumbs=settings.max_breadcrumbs,
88
+ debug=settings.debug,
89
+ send_default_pii=settings.send_default_pii,
90
+ sample_rate=settings.sample_rate,
91
+ traces_sample_rate=settings.traces_sample_rate,
92
+ profiles_sample_rate=settings.profiles_sample_rate,
93
+ integrations=[TyperIntegration()],
94
+ )
95
+
96
+ return True
@@ -0,0 +1,39 @@
1
+ """Base class for services."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, TypeVar
5
+
6
+ from pydantic_settings import BaseSettings
7
+
8
+ from ._health import Health
9
+ from ._settings import load_settings
10
+
11
+ T = TypeVar("T", bound=BaseSettings)
12
+
13
+
14
+ class BaseService(ABC):
15
+ """Base class for services."""
16
+
17
+ _settings: BaseSettings
18
+
19
+ def __init__(self, settings_class: type[T] | None = None) -> None:
20
+ """
21
+ Initialize service with optional settings.
22
+
23
+ Args:
24
+ settings_class: Optional settings class to load configuration.
25
+ """
26
+ if settings_class is not None:
27
+ self._settings = load_settings(settings_class)
28
+
29
+ def key(self) -> str:
30
+ """Return the module name of the instance."""
31
+ return self.__module__.split(".")[-2]
32
+
33
+ @abstractmethod
34
+ def health(self) -> Health:
35
+ """Get health of this service. Override in subclass."""
36
+
37
+ @abstractmethod
38
+ def info(self) -> dict[str, Any]:
39
+ """Get info of this service. Override in subclass."""
@@ -0,0 +1,68 @@
1
+ """Utilities around Pydantic settings."""
2
+
3
+ import json
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import TypeVar
8
+
9
+ from pydantic import ValidationError
10
+ from pydantic_settings import BaseSettings
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+
14
+ from ._console import console
15
+
16
+ T = TypeVar("T", bound=BaseSettings)
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def load_settings(settings_class: type[T]) -> T:
22
+ """
23
+ Load settings with error handling and nice formatting.
24
+
25
+ Args:
26
+ settings_class: The Pydantic settings class to instantiate
27
+
28
+ Returns:
29
+ (T): Instance of the settings class
30
+
31
+ Raises:
32
+ SystemExit: If settings validation fails
33
+ """
34
+ try:
35
+ return settings_class()
36
+ except ValidationError as e:
37
+ errors = json.loads(e.json())
38
+ text = Text()
39
+ text.append(
40
+ "Validation error(s): \n\n",
41
+ style="debug",
42
+ )
43
+
44
+ prefix = settings_class.model_config.get("env_prefix", "")
45
+ for error in errors:
46
+ env_var = f"{prefix}{error['loc'][0]}".upper()
47
+ logger.fatal(f"Configuration invalid! {env_var}: {error['msg']}")
48
+ text.append(f"• {env_var}", style="yellow bold")
49
+ text.append(f": {error['msg']}\n")
50
+
51
+ text.append(
52
+ "\nCheck settings defined in the process environment and in file ",
53
+ style="info",
54
+ )
55
+ env_file = str(settings_class.model_config.get("env_file", ".env") or ".env")
56
+ text.append(
57
+ str(Path(__file__).parent.parent.parent.parent / env_file),
58
+ style="bold blue underline",
59
+ )
60
+
61
+ console.print(
62
+ Panel(
63
+ text,
64
+ title="Configuration invalid!",
65
+ border_style="error",
66
+ ),
67
+ )
68
+ sys.exit(78)