oe-python-template-example 0.3.4__py3-none-any.whl → 0.3.6__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.
- oe_python_template_example/__init__.py +4 -18
- oe_python_template_example/api.py +42 -148
- oe_python_template_example/cli.py +13 -141
- oe_python_template_example/constants.py +6 -9
- oe_python_template_example/hello/__init__.py +17 -0
- oe_python_template_example/hello/_api.py +94 -0
- oe_python_template_example/hello/_cli.py +47 -0
- oe_python_template_example/hello/_constants.py +4 -0
- oe_python_template_example/hello/_models.py +28 -0
- oe_python_template_example/hello/_service.py +96 -0
- oe_python_template_example/{settings.py → hello/_settings.py} +6 -4
- oe_python_template_example/system/__init__.py +19 -0
- oe_python_template_example/system/_api.py +116 -0
- oe_python_template_example/system/_cli.py +165 -0
- oe_python_template_example/system/_service.py +182 -0
- oe_python_template_example/system/_settings.py +31 -0
- oe_python_template_example/utils/__init__.py +59 -0
- oe_python_template_example/utils/_api.py +18 -0
- oe_python_template_example/utils/_cli.py +68 -0
- oe_python_template_example/utils/_console.py +14 -0
- oe_python_template_example/utils/_constants.py +48 -0
- oe_python_template_example/utils/_di.py +70 -0
- oe_python_template_example/utils/_health.py +107 -0
- oe_python_template_example/utils/_log.py +122 -0
- oe_python_template_example/utils/_logfire.py +68 -0
- oe_python_template_example/utils/_process.py +41 -0
- oe_python_template_example/utils/_sentry.py +97 -0
- oe_python_template_example/utils/_service.py +39 -0
- oe_python_template_example/utils/_settings.py +80 -0
- oe_python_template_example/utils/boot.py +86 -0
- {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/METADATA +77 -51
- oe_python_template_example-0.3.6.dist-info/RECORD +35 -0
- oe_python_template_example/models.py +0 -44
- oe_python_template_example/service.py +0 -68
- oe_python_template_example-0.3.4.dist-info/RECORD +0 -12
- {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/WHEEL +0 -0
- {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/entry_points.txt +0 -0
- {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
"""Service of the hello module."""
|
2
|
+
|
3
|
+
import secrets
|
4
|
+
import string
|
5
|
+
from http import HTTPStatus
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
import requests
|
9
|
+
|
10
|
+
from oe_python_template_example.utils import BaseService, Health
|
11
|
+
|
12
|
+
from ._constants import HELLO_WORLD_DE_DE, HELLO_WORLD_EN_US
|
13
|
+
from ._models import Echo, Utterance
|
14
|
+
from ._settings import Language, Settings
|
15
|
+
|
16
|
+
|
17
|
+
# Services derived from BaseService and exported by modules via their __init__.py are automatically registered
|
18
|
+
# with the system module, enabling for dynamic discovery of health, info and further functionality.
|
19
|
+
class Service(BaseService):
|
20
|
+
"""Service of the hello module."""
|
21
|
+
|
22
|
+
_settings: Settings
|
23
|
+
|
24
|
+
def __init__(self) -> None:
|
25
|
+
"""Initialize service."""
|
26
|
+
super().__init__(Settings) # automatically loads and validates the settings
|
27
|
+
|
28
|
+
def info(self) -> dict[str, Any]: # noqa: PLR6301
|
29
|
+
"""Determine info of this service.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
dict[str,Any]: The info of this service.
|
33
|
+
"""
|
34
|
+
random_string = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(5))
|
35
|
+
|
36
|
+
return {"noise": random_string}
|
37
|
+
|
38
|
+
@staticmethod
|
39
|
+
def _determine_connectivity() -> Health:
|
40
|
+
"""Determine healthiness of connectivity with the Internet.
|
41
|
+
|
42
|
+
- Performs HTTP GET request to https://connectivitycheck.gstatic.com/generate_204
|
43
|
+
- If the call fails or does not return the expected response status, the health is DOWN.
|
44
|
+
- If the call succeeds, the health is UP.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Health: The healthiness of connectivity.
|
48
|
+
"""
|
49
|
+
try:
|
50
|
+
response = requests.get("https://connectivitycheck.gstatic.com/generate_204", timeout=5)
|
51
|
+
if response.status_code == HTTPStatus.NO_CONTENT:
|
52
|
+
return Health(status=Health.Code.UP)
|
53
|
+
return Health(status=Health.Code.DOWN, reason=f"Unexpected response status: {response.status_code}")
|
54
|
+
except requests.RequestException as e:
|
55
|
+
return Health(status=Health.Code.DOWN, reason=str(e))
|
56
|
+
|
57
|
+
def health(self) -> Health:
|
58
|
+
"""Determine health of hello service.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Health: The health of the service.
|
62
|
+
"""
|
63
|
+
return Health(
|
64
|
+
status=Health.Code.UP,
|
65
|
+
components={
|
66
|
+
"connectivity": self._determine_connectivity(),
|
67
|
+
},
|
68
|
+
)
|
69
|
+
|
70
|
+
def get_hello_world(self) -> str:
|
71
|
+
"""
|
72
|
+
Get a hello world message.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
str: Hello world message.
|
76
|
+
"""
|
77
|
+
match self._settings.language:
|
78
|
+
case Language.GERMAN:
|
79
|
+
return HELLO_WORLD_DE_DE
|
80
|
+
return HELLO_WORLD_EN_US
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def echo(utterance: Utterance) -> Echo:
|
84
|
+
"""
|
85
|
+
Loudly echo utterance.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
utterance (Utterance): The utterance to echo.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
Echo: The loudly echoed utterance.
|
92
|
+
|
93
|
+
Raises:
|
94
|
+
ValueError: If the utterance is empty or contains only whitespace.
|
95
|
+
"""
|
96
|
+
return Echo(text=utterance.text.upper())
|
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Settings of
|
1
|
+
"""Settings of the hello module."""
|
2
2
|
|
3
3
|
from enum import StrEnum
|
4
4
|
from typing import Annotated
|
@@ -6,7 +6,7 @@ from typing import Annotated
|
|
6
6
|
from pydantic import Field
|
7
7
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
8
8
|
|
9
|
-
from . import __project_name__
|
9
|
+
from oe_python_template_example.utils import __env_file__, __project_name__
|
10
10
|
|
11
11
|
|
12
12
|
class Language(StrEnum):
|
@@ -16,13 +16,15 @@ class Language(StrEnum):
|
|
16
16
|
US_ENGLISH = "en_US"
|
17
17
|
|
18
18
|
|
19
|
+
# Settings derived from BaseSettings and exported by modules via their __init__.py are automatically registered
|
20
|
+
# by the system module e.g. for showing all settings via the system info command.
|
19
21
|
class Settings(BaseSettings):
|
20
22
|
"""Settings."""
|
21
23
|
|
22
24
|
model_config = SettingsConfigDict(
|
23
|
-
env_prefix=f"{__project_name__.upper()}
|
25
|
+
env_prefix=f"{__project_name__.upper()}_HELLO_",
|
24
26
|
extra="ignore",
|
25
|
-
env_file=
|
27
|
+
env_file=__env_file__,
|
26
28
|
env_file_encoding="utf-8",
|
27
29
|
)
|
28
30
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"""Hello module."""
|
2
|
+
|
3
|
+
from ._api import api_routers
|
4
|
+
from ._cli import cli
|
5
|
+
from ._service import Service
|
6
|
+
from ._settings import Settings
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"Service",
|
10
|
+
"Settings",
|
11
|
+
"api_routers",
|
12
|
+
"cli",
|
13
|
+
]
|
14
|
+
|
15
|
+
# Export all individual API routers so they are picked up by depdency injection (DI)
|
16
|
+
for version, router in api_routers.items():
|
17
|
+
router_name = f"api_{version}"
|
18
|
+
globals()[router_name] = router
|
19
|
+
del router
|
@@ -0,0 +1,116 @@
|
|
1
|
+
"""API operations of system module.
|
2
|
+
|
3
|
+
This module provides a webservice API with several operations:
|
4
|
+
- A health/healthz endpoint that returns the health status of the service
|
5
|
+
|
6
|
+
The endpoints use Pydantic models for request and response validation.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from collections.abc import Callable, Generator
|
10
|
+
from typing import Annotated, Any
|
11
|
+
|
12
|
+
from fastapi import APIRouter, Depends, Response, status
|
13
|
+
|
14
|
+
from ..constants import API_VERSIONS # noqa: TID252
|
15
|
+
from ..utils import Health, VersionedAPIRouter # noqa: TID252
|
16
|
+
from ._service import Service
|
17
|
+
|
18
|
+
|
19
|
+
def get_service() -> Generator[Service, None, None]:
|
20
|
+
"""Get instance of Service.
|
21
|
+
|
22
|
+
Yields:
|
23
|
+
Service: The service instance.
|
24
|
+
"""
|
25
|
+
service = Service()
|
26
|
+
try:
|
27
|
+
yield service
|
28
|
+
finally:
|
29
|
+
# Cleanup code if needed
|
30
|
+
pass
|
31
|
+
|
32
|
+
|
33
|
+
def register_health_endpoint(router: APIRouter) -> Callable[..., Health]:
|
34
|
+
"""Register health endpoint to the given router.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
router: The router to register the health endpoint to.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Callable[..., Health]: The health endpoint function.
|
41
|
+
"""
|
42
|
+
|
43
|
+
@router.get("/healthz")
|
44
|
+
@router.get("/system/health")
|
45
|
+
def health_endpoint(service: Annotated[Service, Depends(get_service)], response: Response) -> Health:
|
46
|
+
"""Determine aggregate health of the system.
|
47
|
+
|
48
|
+
The health is aggregated from all modules making
|
49
|
+
up this system including external dependencies.
|
50
|
+
|
51
|
+
The response is to be interpreted as follows:
|
52
|
+
- The status can be either UP or DOWN.
|
53
|
+
- If the service is healthy, the status will be UP.
|
54
|
+
- If the service is unhealthy, the status will be DOWN and a reason will be provided.
|
55
|
+
- The response will have a 200 OK status code if the service is healthy,
|
56
|
+
and a 503 Service Unavailable status code if the service is unhealthy.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
service (Service): The service instance.
|
60
|
+
response (Response): The FastAPI response object.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Health: The health of the system.
|
64
|
+
"""
|
65
|
+
health = service.health()
|
66
|
+
if health.status == Health.Code.DOWN:
|
67
|
+
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
68
|
+
|
69
|
+
return health
|
70
|
+
|
71
|
+
return health_endpoint
|
72
|
+
|
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
|
+
|
111
|
+
api_routers = {}
|
112
|
+
for version in API_VERSIONS:
|
113
|
+
router = VersionedAPIRouter(version, tags=["system"])
|
114
|
+
api_routers[version] = router
|
115
|
+
health = register_health_endpoint(api_routers[version])
|
116
|
+
info = register_info_endpoint(api_routers[version])
|
@@ -0,0 +1,165 @@
|
|
1
|
+
"""System CLI commands."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
from enum import StrEnum
|
6
|
+
from typing import Annotated
|
7
|
+
|
8
|
+
import typer
|
9
|
+
import uvicorn
|
10
|
+
import yaml
|
11
|
+
|
12
|
+
from ..constants import API_VERSIONS # noqa: TID252
|
13
|
+
from ..utils import __project_name__, console, get_logger # noqa: TID252
|
14
|
+
from ._service import Service
|
15
|
+
|
16
|
+
logger = get_logger(__name__)
|
17
|
+
|
18
|
+
cli = typer.Typer(name="system", help="System commands")
|
19
|
+
|
20
|
+
_service = Service()
|
21
|
+
|
22
|
+
|
23
|
+
class OutputFormat(StrEnum):
|
24
|
+
"""
|
25
|
+
Enum representing the supported output formats.
|
26
|
+
|
27
|
+
This enum defines the possible formats for output data:
|
28
|
+
- YAML: Output data in YAML format
|
29
|
+
- JSON: Output data in JSON format
|
30
|
+
|
31
|
+
Usage:
|
32
|
+
format = OutputFormat.YAML
|
33
|
+
print(f"Using {format} format")
|
34
|
+
"""
|
35
|
+
|
36
|
+
YAML = "yaml"
|
37
|
+
JSON = "json"
|
38
|
+
|
39
|
+
|
40
|
+
@cli.command()
|
41
|
+
def health(
|
42
|
+
output_format: Annotated[
|
43
|
+
OutputFormat, typer.Option(help="Output format", case_sensitive=False)
|
44
|
+
] = OutputFormat.JSON,
|
45
|
+
) -> None:
|
46
|
+
"""Determine and print system health.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
output_format (OutputFormat): Output format (JSON or YAML).
|
50
|
+
"""
|
51
|
+
match output_format:
|
52
|
+
case OutputFormat.JSON:
|
53
|
+
console.print_json(data=_service.health().model_dump())
|
54
|
+
case OutputFormat.YAML:
|
55
|
+
console.print(
|
56
|
+
yaml.dump(data=json.loads(_service.health().model_dump_json()), width=80, default_flow_style=False),
|
57
|
+
end="",
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
@cli.command()
|
62
|
+
def info(
|
63
|
+
include_environ: Annotated[bool, typer.Option(help="Include environment variables")] = False,
|
64
|
+
filter_secrets: Annotated[bool, typer.Option(help="Filter secrets")] = True,
|
65
|
+
output_format: Annotated[
|
66
|
+
OutputFormat, typer.Option(help="Output format", case_sensitive=False)
|
67
|
+
] = OutputFormat.JSON,
|
68
|
+
) -> None:
|
69
|
+
"""Determine and print system info.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
include_environ (bool): Include environment variables.
|
73
|
+
filter_secrets (bool): Filter secrets from the output.
|
74
|
+
output_format (OutputFormat): Output format (JSON or YAML).
|
75
|
+
"""
|
76
|
+
info = _service.info(include_environ=include_environ, filter_secrets=filter_secrets)
|
77
|
+
match output_format:
|
78
|
+
case OutputFormat.JSON:
|
79
|
+
console.print_json(data=info)
|
80
|
+
case OutputFormat.YAML:
|
81
|
+
console.print(yaml.dump(info, width=80, default_flow_style=False), end="")
|
82
|
+
|
83
|
+
|
84
|
+
@cli.command()
|
85
|
+
def serve(
|
86
|
+
host: Annotated[str, typer.Option(help="Host to bind the server to")] = "127.0.0.1",
|
87
|
+
port: Annotated[int, typer.Option(help="Port to bind the server to")] = 8000,
|
88
|
+
watch: Annotated[bool, typer.Option(help="Enable auto-reload")] = True,
|
89
|
+
) -> None:
|
90
|
+
"""Start the webservice API server.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
host (str): Host to bind the server to.
|
94
|
+
port (int): Port to bind the server to.
|
95
|
+
watch (bool): Enable auto-reload.
|
96
|
+
"""
|
97
|
+
console.print(f"Starting API server at http://{host}:{port}")
|
98
|
+
# using environ to pass host/port to api.py to generate doc link
|
99
|
+
os.environ["UVICORN_HOST"] = host
|
100
|
+
os.environ["UVICORN_PORT"] = str(port)
|
101
|
+
uvicorn.run(
|
102
|
+
f"{__project_name__}.api:app",
|
103
|
+
host=host,
|
104
|
+
port=port,
|
105
|
+
reload=watch,
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
@cli.command()
|
110
|
+
def openapi(
|
111
|
+
api_version: Annotated[
|
112
|
+
str, typer.Option(help=f"API Version. Available: {', '.join(API_VERSIONS.keys())}", case_sensitive=False)
|
113
|
+
] = next(iter(API_VERSIONS.keys())),
|
114
|
+
output_format: Annotated[
|
115
|
+
OutputFormat, typer.Option(help="Output format", case_sensitive=False)
|
116
|
+
] = OutputFormat.JSON,
|
117
|
+
) -> None:
|
118
|
+
"""Dump the OpenAPI specification.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
api_version (str): API version to dump.
|
122
|
+
output_format (OutputFormat): Output format (JSON or YAML).
|
123
|
+
|
124
|
+
Raises:
|
125
|
+
typer.Exit: If an invalid API version is provided.
|
126
|
+
"""
|
127
|
+
from ..api import api_instances # noqa: PLC0415, TID252
|
128
|
+
|
129
|
+
if api_version not in API_VERSIONS:
|
130
|
+
available_versions = ", ".join(API_VERSIONS.keys())
|
131
|
+
console.print(
|
132
|
+
f"[bold red]Error:[/] Invalid API version '{api_version}'. Available versions: {available_versions}"
|
133
|
+
)
|
134
|
+
raise typer.Exit(code=1)
|
135
|
+
|
136
|
+
schema = api_instances[api_version].openapi()
|
137
|
+
|
138
|
+
match output_format:
|
139
|
+
case OutputFormat.JSON:
|
140
|
+
console.print_json(data=schema)
|
141
|
+
case OutputFormat.YAML:
|
142
|
+
console.print(yaml.dump(schema, width=80, default_flow_style=False), end="")
|
143
|
+
|
144
|
+
|
145
|
+
@cli.command()
|
146
|
+
def fail() -> None:
|
147
|
+
"""Fail by dividing by zero.
|
148
|
+
|
149
|
+
- Used to validate error handling and instrumentation.
|
150
|
+
"""
|
151
|
+
Service.div_by_zero()
|
152
|
+
|
153
|
+
|
154
|
+
@cli.command()
|
155
|
+
def sleep(
|
156
|
+
seconds: Annotated[int, typer.Option(help="Duration in seconds")] = 10,
|
157
|
+
) -> None:
|
158
|
+
"""Sleep given for given number of seconds.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
seconds (int): Number of seconds to sleep.
|
162
|
+
|
163
|
+
- Used to validate performance profiling.
|
164
|
+
"""
|
165
|
+
Service.sleep(seconds)
|
@@ -0,0 +1,182 @@
|
|
1
|
+
"""System service."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import platform
|
6
|
+
import pwd
|
7
|
+
import sys
|
8
|
+
import time
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
from pydantic_settings import BaseSettings
|
12
|
+
|
13
|
+
from ..utils import ( # noqa: TID252
|
14
|
+
UNHIDE_SENSITIVE_INFO,
|
15
|
+
BaseService,
|
16
|
+
Health,
|
17
|
+
__env__,
|
18
|
+
__project_name__,
|
19
|
+
__project_path__,
|
20
|
+
__repository_url__,
|
21
|
+
__version__,
|
22
|
+
get_logger,
|
23
|
+
get_process_info,
|
24
|
+
load_settings,
|
25
|
+
locate_subclasses,
|
26
|
+
)
|
27
|
+
from ._settings import Settings
|
28
|
+
|
29
|
+
logger = get_logger(__name__)
|
30
|
+
|
31
|
+
|
32
|
+
class Service(BaseService):
|
33
|
+
"""System service."""
|
34
|
+
|
35
|
+
_settings: Settings
|
36
|
+
|
37
|
+
def __init__(self) -> None:
|
38
|
+
"""Initialize service."""
|
39
|
+
super().__init__(Settings)
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def _is_healthy() -> bool:
|
43
|
+
"""Check if the service itself is healthy.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
bool: True if the service is healthy, False otherwise.
|
47
|
+
"""
|
48
|
+
return True
|
49
|
+
|
50
|
+
def health(self) -> Health:
|
51
|
+
"""Determine aggregate health of the system.
|
52
|
+
|
53
|
+
- Health exposed by implementations of BaseService in other
|
54
|
+
modules is automatically included into the health tree.
|
55
|
+
- See utils/_health.py:Health for an explanation of the health tree.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
Health: The aggregate health of the system.
|
59
|
+
"""
|
60
|
+
components: dict[str, Health] = {}
|
61
|
+
for service_class in locate_subclasses(BaseService):
|
62
|
+
if service_class is not Service:
|
63
|
+
components[f"{service_class.__module__}.{service_class.__name__}"] = service_class().health()
|
64
|
+
|
65
|
+
# Set the system health status based on is_healthy attribute
|
66
|
+
status = Health.Code.UP if self._is_healthy() else Health.Code.DOWN
|
67
|
+
reason = None if self._is_healthy() else "System marked as unhealthy"
|
68
|
+
return Health(status=status, components=components, reason=reason)
|
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
|
+
|
82
|
+
@staticmethod
|
83
|
+
def info(include_environ: bool = False, filter_secrets: bool = True) -> dict[str, Any]:
|
84
|
+
"""
|
85
|
+
Get info about configuration of service.
|
86
|
+
|
87
|
+
- Runtime information is automatically compiled.
|
88
|
+
- Settings are automatically aggregated from all implementations of
|
89
|
+
Pydantic BaseSettings in this package.
|
90
|
+
- Info exposed by implementations of BaseService in other modules is
|
91
|
+
automatically included into the info dict.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
dict[str, Any]: Service configuration.
|
95
|
+
"""
|
96
|
+
rtn = {
|
97
|
+
"package": {
|
98
|
+
"version": __version__,
|
99
|
+
"name": __project_name__,
|
100
|
+
"repository": __repository_url__,
|
101
|
+
"local": __project_path__,
|
102
|
+
},
|
103
|
+
"runtime": {
|
104
|
+
"environment": __env__,
|
105
|
+
"python": {
|
106
|
+
"version": platform.python_version(),
|
107
|
+
"compiler": platform.python_compiler(),
|
108
|
+
"implementation": platform.python_implementation(),
|
109
|
+
},
|
110
|
+
"interpreter_path": sys.executable,
|
111
|
+
"command_line": " ".join(sys.argv),
|
112
|
+
"entry_point": sys.argv[0] if sys.argv else None,
|
113
|
+
"process_info": json.loads(get_process_info().model_dump_json()),
|
114
|
+
"username": pwd.getpwuid(os.getuid())[0],
|
115
|
+
"host": {
|
116
|
+
"system": platform.system(),
|
117
|
+
"release": platform.release(),
|
118
|
+
"version": platform.version(),
|
119
|
+
"machine": platform.machine(),
|
120
|
+
"processor": platform.processor(),
|
121
|
+
"hostname": platform.node(),
|
122
|
+
"ip_address": platform.uname().node,
|
123
|
+
"cpu_count": os.cpu_count(),
|
124
|
+
},
|
125
|
+
},
|
126
|
+
}
|
127
|
+
|
128
|
+
if include_environ:
|
129
|
+
if filter_secrets:
|
130
|
+
rtn["runtime"]["environ"] = {
|
131
|
+
k: v
|
132
|
+
for k, v in os.environ.items()
|
133
|
+
if not (
|
134
|
+
"token" in k.lower()
|
135
|
+
or "key" in k.lower()
|
136
|
+
or "secret" in k.lower()
|
137
|
+
or "password" in k.lower()
|
138
|
+
or "auth" in k.lower()
|
139
|
+
)
|
140
|
+
}
|
141
|
+
else:
|
142
|
+
rtn["runtime"]["environ"] = dict(os.environ)
|
143
|
+
|
144
|
+
settings = {}
|
145
|
+
for settings_class in locate_subclasses(BaseSettings):
|
146
|
+
settings_instance = load_settings(settings_class)
|
147
|
+
env_prefix = settings_instance.model_config.get("env_prefix", "")
|
148
|
+
settings_dict = settings_instance.model_dump(context={UNHIDE_SENSITIVE_INFO: not filter_secrets})
|
149
|
+
for key, value in settings_dict.items():
|
150
|
+
flat_key = f"{env_prefix}{key}".upper()
|
151
|
+
settings[flat_key] = value
|
152
|
+
rtn["settings"] = settings
|
153
|
+
|
154
|
+
for service_class in locate_subclasses(BaseService):
|
155
|
+
if service_class is not Service:
|
156
|
+
service = service_class()
|
157
|
+
rtn[service.key()] = service.info()
|
158
|
+
|
159
|
+
return rtn
|
160
|
+
|
161
|
+
@staticmethod
|
162
|
+
def div_by_zero() -> float:
|
163
|
+
"""Divide by zero to trigger an error.
|
164
|
+
|
165
|
+
- This function is used to validate error handling and instrumentation
|
166
|
+
in the system.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
float: This function will raise a ZeroDivisionError before returning.
|
170
|
+
"""
|
171
|
+
return 1 / 0
|
172
|
+
|
173
|
+
@staticmethod
|
174
|
+
def sleep(seconds: int) -> None:
|
175
|
+
"""Sleep for a given number of seconds.
|
176
|
+
|
177
|
+
- This function is used to validate performance profiling in the system.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
seconds (int): Number of seconds to sleep.
|
181
|
+
"""
|
182
|
+
time.sleep(seconds)
|
@@ -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
|
+
]
|