oe-python-template 0.10.1__py3-none-any.whl → 0.10.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.
@@ -23,7 +23,7 @@ class Service(BaseService):
23
23
 
24
24
  def __init__(self) -> None:
25
25
  """Initialize service."""
26
- super().__init__(Settings)
26
+ super().__init__(Settings) # automatically loads and validates the settings
27
27
 
28
28
  def info(self) -> dict[str, Any]: # noqa: PLR6301
29
29
  """Determine info of this service.
@@ -3,9 +3,11 @@
3
3
  from ._api import api_routers
4
4
  from ._cli import cli
5
5
  from ._service import Service
6
+ from ._settings import Settings
6
7
 
7
8
  __all__ = [
8
9
  "Service",
10
+ "Settings",
9
11
  "api_routers",
10
12
  "cli",
11
13
  ]
@@ -7,7 +7,7 @@ The endpoints use Pydantic models for request and response validation.
7
7
  """
8
8
 
9
9
  from collections.abc import Callable, Generator
10
- from typing import Annotated
10
+ from typing import Annotated, Any
11
11
 
12
12
  from fastapi import APIRouter, Depends, Response, status
13
13
 
@@ -41,11 +41,11 @@ def register_health_endpoint(router: APIRouter) -> Callable[..., Health]:
41
41
  """
42
42
 
43
43
  @router.get("/healthz")
44
- @router.get("/health")
44
+ @router.get("/system/health")
45
45
  def health_endpoint(service: Annotated[Service, Depends(get_service)], response: Response) -> Health:
46
46
  """Determine aggregate health of the system.
47
47
 
48
- The health is aggregated from all modules that make
48
+ The health is aggregated from all modules making
49
49
  up this system including external dependencies.
50
50
 
51
51
  The response is to be interpreted as follows:
@@ -71,8 +71,46 @@ def register_health_endpoint(router: APIRouter) -> Callable[..., Health]:
71
71
  return health_endpoint
72
72
 
73
73
 
74
+ def register_info_endpoint(router: APIRouter) -> Callable[..., dict[str, Any]]:
75
+ """Register info endpoint to the given router.
76
+
77
+ Args:
78
+ router: The router to register the info endpoint to.
79
+
80
+ Returns:
81
+ Callable[..., Health]: The health endpoint function.
82
+ """
83
+
84
+ @router.get("/system/info")
85
+ def info_endpoint(
86
+ service: Annotated[Service, Depends(get_service)], response: Response, token: str
87
+ ) -> dict[str, Any]:
88
+ """Determine aggregate info of the system.
89
+
90
+ The info is aggregated from all modules making up this system.
91
+
92
+ If the token does not match the setting, a 403 Forbidden status code is returned.
93
+
94
+ Args:
95
+ service (Service): The service instance.
96
+ response (Response): The FastAPI response object.
97
+ token (str): Token to present.
98
+
99
+ Returns:
100
+ dict[str, Any]: The aggregate info of the system.
101
+ """
102
+ if service.is_token_valid(token):
103
+ return service.info(include_environ=True, filter_secrets=False)
104
+
105
+ response.status_code = status.HTTP_403_FORBIDDEN
106
+ return {"error": "Forbidden"}
107
+
108
+ return info_endpoint
109
+
110
+
74
111
  api_routers = {}
75
112
  for version in API_VERSIONS:
76
113
  router = VersionedAPIRouter(version, tags=["system"])
77
114
  api_routers[version] = router
78
115
  health = register_health_endpoint(api_routers[version])
116
+ info = register_info_endpoint(api_routers[version])
@@ -11,6 +11,7 @@ from typing import Any
11
11
  from pydantic_settings import BaseSettings
12
12
 
13
13
  from ..utils import ( # noqa: TID252
14
+ UNHIDE_SENSITIVE_INFO,
14
15
  BaseService,
15
16
  Health,
16
17
  __env__,
@@ -18,18 +19,24 @@ from ..utils import ( # noqa: TID252
18
19
  __project_path__,
19
20
  __repository_url__,
20
21
  __version__,
22
+ get_logger,
21
23
  get_process_info,
22
24
  load_settings,
23
25
  locate_subclasses,
24
26
  )
27
+ from ._settings import Settings
28
+
29
+ logger = get_logger(__name__)
25
30
 
26
31
 
27
32
  class Service(BaseService):
28
33
  """System service."""
29
34
 
35
+ _settings: Settings
36
+
30
37
  def __init__(self) -> None:
31
38
  """Initialize service."""
32
- super().__init__()
39
+ super().__init__(Settings)
33
40
 
34
41
  @staticmethod
35
42
  def _is_healthy() -> bool:
@@ -60,6 +67,18 @@ class Service(BaseService):
60
67
  reason = None if self._is_healthy() else "System marked as unhealthy"
61
68
  return Health(status=status, components=components, reason=reason)
62
69
 
70
+ def is_token_valid(self, token: str) -> bool:
71
+ """Check if the presented token is valid.
72
+
73
+ Returns:
74
+ bool: True if the token is valid, False otherwise.
75
+ """
76
+ logger.info(token)
77
+ if not self._settings.token:
78
+ logger.warning("Token is not set in settings.")
79
+ return False
80
+ return token == self._settings.token.get_secret_value()
81
+
63
82
  @staticmethod
64
83
  def info(include_environ: bool = False, filter_secrets: bool = True) -> dict[str, Any]:
65
84
  """
@@ -126,7 +145,7 @@ class Service(BaseService):
126
145
  for settings_class in locate_subclasses(BaseSettings):
127
146
  settings_instance = load_settings(settings_class)
128
147
  env_prefix = settings_instance.model_config.get("env_prefix", "")
129
- settings_dict = json.loads(settings_instance.model_dump_json())
148
+ settings_dict = settings_instance.model_dump(context={UNHIDE_SENSITIVE_INFO: not filter_secrets})
130
149
  for key, value in settings_dict.items():
131
150
  flat_key = f"{env_prefix}{key}".upper()
132
151
  settings[flat_key] = value
@@ -0,0 +1,31 @@
1
+ """Settings of the system module."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field, PlainSerializer, SecretStr
6
+ from pydantic_settings import SettingsConfigDict
7
+
8
+ from ..utils import OpaqueSettings, __env_file__, __project_name__ # noqa: TID252
9
+
10
+
11
+ class Settings(OpaqueSettings):
12
+ """Settings."""
13
+
14
+ model_config = SettingsConfigDict(
15
+ env_prefix=f"{__project_name__.upper()}_SYSTEM_",
16
+ extra="ignore",
17
+ env_file=__env_file__,
18
+ env_file_encoding="utf-8",
19
+ )
20
+
21
+ token: Annotated[
22
+ SecretStr | None,
23
+ PlainSerializer(func=OpaqueSettings.serialize_sensitive_info, return_type=str, when_used="always"),
24
+ Field(
25
+ description=(
26
+ "Secret token to present when performing sensitive operations such as "
27
+ "retrieving info via webservice API"
28
+ ),
29
+ default=None,
30
+ ),
31
+ ]
@@ -23,15 +23,17 @@ from ._logfire import LogfireSettings
23
23
  from ._process import ProcessInfo, get_process_info
24
24
  from ._sentry import SentrySettings
25
25
  from ._service import BaseService
26
- from ._settings import load_settings
26
+ from ._settings import UNHIDE_SENSITIVE_INFO, OpaqueSettings, load_settings
27
27
  from .boot import boot
28
28
 
29
29
  __all__ = [
30
+ "UNHIDE_SENSITIVE_INFO",
30
31
  "BaseService",
31
32
  "Health",
32
33
  "LogSettings",
33
34
  "LogSettings",
34
35
  "LogfireSettings",
36
+ "OpaqueSettings",
35
37
  "ProcessInfo",
36
38
  "SentrySettings",
37
39
  "VersionedAPIRouter",
@@ -10,7 +10,7 @@ __project_path__ = str(Path(__file__).parent.parent.parent)
10
10
  __version__ = metadata.version(__project_name__)
11
11
  __is_development_mode__ = "uvx" not in sys.argv[0].lower()
12
12
  __is_running_in_container__ = os.getenv(f"{__project_name__.upper()}_RUNNING_IN_CONTAINER")
13
- __env__ = os.getenv("ENV", "local")
13
+ __env__ = os.getenv("ENV", os.getenv("VERCEL_ENV", "local"))
14
14
  __env_file__ = [
15
15
  Path.home() / f".{__project_name__}" / ".env",
16
16
  Path.home() / f".{__project_name__}" / f".env.{__env__}",
@@ -3,14 +3,14 @@
3
3
  from typing import Annotated
4
4
 
5
5
  import logfire
6
- from pydantic import Field, SecretStr
7
- from pydantic_settings import BaseSettings, SettingsConfigDict
6
+ from pydantic import Field, PlainSerializer, SecretStr
7
+ from pydantic_settings import SettingsConfigDict
8
8
 
9
9
  from ._constants import __env__, __env_file__, __project_name__, __repository_url__, __version__
10
- from ._settings import load_settings
10
+ from ._settings import OpaqueSettings, load_settings
11
11
 
12
12
 
13
- class LogfireSettings(BaseSettings):
13
+ class LogfireSettings(OpaqueSettings):
14
14
  """Configuration settings for Logfire integration."""
15
15
 
16
16
  model_config = SettingsConfigDict(
@@ -22,6 +22,7 @@ class LogfireSettings(BaseSettings):
22
22
 
23
23
  token: Annotated[
24
24
  SecretStr | None,
25
+ PlainSerializer(func=OpaqueSettings.serialize_sensitive_info, return_type=str, when_used="always"),
25
26
  Field(description="Logfire token. Leave empty to disable logfire.", examples=["YOUR_TOKEN"], default=None),
26
27
  ]
27
28
  instrument_system_metrics: Annotated[
@@ -3,15 +3,15 @@
3
3
  from typing import Annotated
4
4
 
5
5
  import sentry_sdk
6
- from pydantic import Field, SecretStr
7
- from pydantic_settings import BaseSettings, SettingsConfigDict
6
+ from pydantic import Field, PlainSerializer, SecretStr
7
+ from pydantic_settings import SettingsConfigDict
8
8
  from sentry_sdk.integrations.typer import TyperIntegration
9
9
 
10
10
  from ._constants import __env__, __env_file__, __project_name__, __version__
11
- from ._settings import load_settings
11
+ from ._settings import OpaqueSettings, load_settings
12
12
 
13
13
 
14
- class SentrySettings(BaseSettings):
14
+ class SentrySettings(OpaqueSettings):
15
15
  """Configuration settings for Sentry integration."""
16
16
 
17
17
  model_config = SettingsConfigDict(
@@ -23,6 +23,7 @@ class SentrySettings(BaseSettings):
23
23
 
24
24
  dsn: Annotated[
25
25
  SecretStr | None,
26
+ PlainSerializer(func=OpaqueSettings.serialize_sensitive_info, return_type=str, when_used="always"),
26
27
  Field(description="Sentry DSN", examples=["https://SECRET@SECRET.ingest.de.sentry.io/SECRET"], default=None),
27
28
  ]
28
29
 
@@ -6,7 +6,7 @@ import sys
6
6
  from pathlib import Path
7
7
  from typing import TypeVar
8
8
 
9
- from pydantic import ValidationError
9
+ from pydantic import FieldSerializationInfo, SecretStr, ValidationError
10
10
  from pydantic_settings import BaseSettings
11
11
  from rich.panel import Panel
12
12
  from rich.text import Text
@@ -17,6 +17,18 @@ T = TypeVar("T", bound=BaseSettings)
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
+ UNHIDE_SENSITIVE_INFO = "unhide_sensitive_info"
21
+
22
+
23
+ class OpaqueSettings(BaseSettings):
24
+ @staticmethod
25
+ def serialize_sensitive_info(input_value: SecretStr, info: FieldSerializationInfo) -> str | None:
26
+ if not input_value:
27
+ return None
28
+ if info.context.get(UNHIDE_SENSITIVE_INFO, False): # type: ignore
29
+ return input_value.get_secret_value()
30
+ return str(input_value)
31
+
20
32
 
21
33
  def load_settings(settings_class: type[T]) -> T:
22
34
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oe-python-template
3
- Version: 0.10.1
3
+ Version: 0.10.3
4
4
  Summary: 🧠 Copier template to scaffold Python projects compliant with best practices and modern tooling.
5
5
  Project-URL: Homepage, https://oe-python-template.readthedocs.io/en/latest/
6
6
  Project-URL: Documentation, https://oe-python-template.readthedocs.io/en/latest/
@@ -7,28 +7,29 @@ oe_python_template/hello/_api.py,sha256=hWWlEDUfFY1se2ZzhqGMfPyD3FPwuA-YzxG9Q9z4
7
7
  oe_python_template/hello/_cli.py,sha256=mSNmRj_VRoRfCAR1I8tssZqZCTmT6jgzsNWrTtlFP7Y,1184
8
8
  oe_python_template/hello/_constants.py,sha256=6aRleAIcdgC13TeTzI07YwjoSwqGb2g131dw8aEoM4I,109
9
9
  oe_python_template/hello/_models.py,sha256=JtI7wGT72u23NOxFa-oeWzdyiMg7PnHL5eg22im2_yQ,574
10
- oe_python_template/hello/_service.py,sha256=-rUlt54EAafMhLSo-IBsWMe6mjLTIlYfhxCFKVl8C8s,3004
10
+ oe_python_template/hello/_service.py,sha256=zI5lXKdI28D1uIqY0phecnlJPxCrwtQVO6d7w1HstS8,3054
11
11
  oe_python_template/hello/_settings.py,sha256=1z77_B4pWFx_ag9EasgUM01MjKeBwiAhnkhL9XjlR9I,984
12
- oe_python_template/system/__init__.py,sha256=u6UcW5_-5hT1Iqlv2NrDGW2QYVCryu2_bDkJKxl9z-c,379
13
- oe_python_template/system/_api.py,sha256=-uorLNHyDHKll9IUp_1fadMQ-BATXTK33gdmfflTaWo,2444
12
+ oe_python_template/system/__init__.py,sha256=NNgODkr7AyJjTTJiv3pys7o2z6xi1G96g0vnsxVhlI4,427
13
+ oe_python_template/system/_api.py,sha256=rE9Aau3IIHXdEkOBUXOwJ7SxN3cZpgtYEuojnSWfT_4,3687
14
14
  oe_python_template/system/_cli.py,sha256=J_4upBBdbSxbONPYmOZPbuhZcKjfnPIUeZpp0L7lY-Q,4846
15
- oe_python_template/system/_service.py,sha256=PSM22bV-0MYnh3KI41z0UiGhzWpvJBBMzFflwrRMw4U,5513
16
- oe_python_template/utils/__init__.py,sha256=XCu2HmU4JHzd4h5wU9lwNI7CSt1uf9sSh358RTtGAF0,1359
15
+ oe_python_template/system/_service.py,sha256=G8Us2ez4MF7NWdWOUBbcxaVT3dpM-QZ_dP9b_1MkjIQ,6093
16
+ oe_python_template/system/_settings.py,sha256=MwMAJYifJ6jGImeSh4e9shmIXmiUSuQGHXz_Ts0mSdk,901
17
+ oe_python_template/utils/__init__.py,sha256=rHdmSS21CtvF3AXPMSCZO17FTzxPDo-NiKZ5AjVU9b0,1449
17
18
  oe_python_template/utils/_api.py,sha256=w3hPQK1pL2gBI4_1qNWNa2b4S_oH-8mY-ckRX0KrCWM,617
18
19
  oe_python_template/utils/_cli.py,sha256=J_mFtXZ1gGeovGrE5i3wlokTOBfiTTKEz5magiRP7GA,2091
19
20
  oe_python_template/utils/_console.py,sha256=u0-utcdRmVu4rabrYUyNOx8yPxLhxB3E92m22kSCwPQ,293
20
- oe_python_template/utils/_constants.py,sha256=72gJp53wLIyorzGXdA1h6alXmaUWpvEVOXZ8EJ1HOIA,1712
21
+ oe_python_template/utils/_constants.py,sha256=1ocbciHjhmy66uHrTw6p-fbBq_wLl1HaUSu4oTgt8mo,1737
21
22
  oe_python_template/utils/_di.py,sha256=KdjiD4xZ_QSfbddkKWwsPJmG5YrIg6dzuBrlsd-FhxA,2189
22
23
  oe_python_template/utils/_health.py,sha256=35QOWe2r5InrEpGtuVMym9dI5aRHS0HWf4BHBRAUIj0,4102
23
24
  oe_python_template/utils/_log.py,sha256=ZW4gs540SdjVK-2KeheLfDY15d_3xpO5FyGn7wTXyaM,3592
24
- oe_python_template/utils/_logfire.py,sha256=zLkrgukvJpPZ8vQpawgDViPMMWza1APFlIIlKVny_ak,1893
25
+ oe_python_template/utils/_logfire.py,sha256=g2tR60XgpdRs_UwDpqkwWm6ErUBcV7lPFBJdvgfTuu0,2022
25
26
  oe_python_template/utils/_process.py,sha256=40R0NZMqJUn0iUPERzohSUpJgU1HcJApIg1HipIxFCw,941
26
- oe_python_template/utils/_sentry.py,sha256=ChSvJu-B_oby5F6oqycJ6McYzy15jRj9yZgG5apOsY4,2999
27
+ oe_python_template/utils/_sentry.py,sha256=Y4hZ-PeBOdR3iRhoXW9j0tbWsYf07460UG8OVTKH1mU,3128
27
28
  oe_python_template/utils/_service.py,sha256=atHAejvBucKXjzhsMSdOBBFa7rRD74zcV70Pp0pl0Tg,1038
28
- oe_python_template/utils/_settings.py,sha256=lA47dtwMHgO62j3R6i1W7sTidrQwVNloOGGxbS2rz6Q,1843
29
+ oe_python_template/utils/_settings.py,sha256=5K1pnp-AxMQbktREb3bXDmqgrOx_L4EJIgjPQfqH4sE,2294
29
30
  oe_python_template/utils/boot.py,sha256=TBgmqbtIryQz0cAozYzxhYQRIldfbJ6v9R-rH6sO9mY,2696
30
- oe_python_template-0.10.1.dist-info/METADATA,sha256=2rP1g7b8hqv0W2yGu0GEXgn4zjSQE552F7GH7aV2-HI,31916
31
- oe_python_template-0.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
32
- oe_python_template-0.10.1.dist-info/entry_points.txt,sha256=IroSSWhLGxus9rxcashkYQda39TTvf7LbUMYtOKXUBE,66
33
- oe_python_template-0.10.1.dist-info/licenses/LICENSE,sha256=5H409K6xzz9U5eUaoAHQExNkoWJRlU0LEj6wL2QJ34s,1113
34
- oe_python_template-0.10.1.dist-info/RECORD,,
31
+ oe_python_template-0.10.3.dist-info/METADATA,sha256=lembvVaYyUa62VqmgNc_Zd1w6Twfo_9V0BWsX4t6OT8,31916
32
+ oe_python_template-0.10.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ oe_python_template-0.10.3.dist-info/entry_points.txt,sha256=IroSSWhLGxus9rxcashkYQda39TTvf7LbUMYtOKXUBE,66
34
+ oe_python_template-0.10.3.dist-info/licenses/LICENSE,sha256=5H409K6xzz9U5eUaoAHQExNkoWJRlU0LEj6wL2QJ34s,1113
35
+ oe_python_template-0.10.3.dist-info/RECORD,,