oe-python-template-example 0.3.5__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.
Files changed (38) hide show
  1. oe_python_template_example/__init__.py +4 -18
  2. oe_python_template_example/api.py +42 -148
  3. oe_python_template_example/cli.py +13 -141
  4. oe_python_template_example/constants.py +6 -9
  5. oe_python_template_example/hello/__init__.py +17 -0
  6. oe_python_template_example/hello/_api.py +94 -0
  7. oe_python_template_example/hello/_cli.py +47 -0
  8. oe_python_template_example/hello/_constants.py +4 -0
  9. oe_python_template_example/hello/_models.py +28 -0
  10. oe_python_template_example/hello/_service.py +96 -0
  11. oe_python_template_example/{settings.py → hello/_settings.py} +6 -4
  12. oe_python_template_example/system/__init__.py +19 -0
  13. oe_python_template_example/system/_api.py +116 -0
  14. oe_python_template_example/system/_cli.py +165 -0
  15. oe_python_template_example/system/_service.py +182 -0
  16. oe_python_template_example/system/_settings.py +31 -0
  17. oe_python_template_example/utils/__init__.py +59 -0
  18. oe_python_template_example/utils/_api.py +18 -0
  19. oe_python_template_example/utils/_cli.py +68 -0
  20. oe_python_template_example/utils/_console.py +14 -0
  21. oe_python_template_example/utils/_constants.py +48 -0
  22. oe_python_template_example/utils/_di.py +70 -0
  23. oe_python_template_example/utils/_health.py +107 -0
  24. oe_python_template_example/utils/_log.py +122 -0
  25. oe_python_template_example/utils/_logfire.py +68 -0
  26. oe_python_template_example/utils/_process.py +41 -0
  27. oe_python_template_example/utils/_sentry.py +97 -0
  28. oe_python_template_example/utils/_service.py +39 -0
  29. oe_python_template_example/utils/_settings.py +80 -0
  30. oe_python_template_example/utils/boot.py +86 -0
  31. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.6.dist-info}/METADATA +77 -51
  32. oe_python_template_example-0.3.6.dist-info/RECORD +35 -0
  33. oe_python_template_example/models.py +0 -44
  34. oe_python_template_example/service.py +0 -68
  35. oe_python_template_example-0.3.5.dist-info/RECORD +0 -12
  36. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.6.dist-info}/WHEEL +0 -0
  37. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.6.dist-info}/entry_points.txt +0 -0
  38. {oe_python_template_example-0.3.5.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 OE Python Template Example."""
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=".env",
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
+ ]