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,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.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)
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."""
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.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,17 @@
1
+ """Hello module."""
2
+
3
+ from ._api import api_routers
4
+ from ._cli import cli
5
+ from ._service import Service
6
+
7
+ __all__ = [
8
+ "Service",
9
+ "api_routers",
10
+ "cli",
11
+ ]
12
+
13
+ # Export all individual API routers so they are picked up by depdency injection (DI)
14
+ for version, router in api_routers.items():
15
+ router_name = f"api_{version}"
16
+ globals()[router_name] = router
17
+ del router
@@ -0,0 +1,78 @@
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
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("/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 that make
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
+ api_routers = {}
75
+ for version in API_VERSIONS:
76
+ router = VersionedAPIRouter(version, tags=["system"])
77
+ api_routers[version] = router
78
+ health = register_health_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,163 @@
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
+ BaseService,
15
+ Health,
16
+ __env__,
17
+ __project_name__,
18
+ __project_path__,
19
+ __repository_url__,
20
+ __version__,
21
+ get_process_info,
22
+ load_settings,
23
+ locate_subclasses,
24
+ )
25
+
26
+
27
+ class Service(BaseService):
28
+ """System service."""
29
+
30
+ def __init__(self) -> None:
31
+ """Initialize service."""
32
+ super().__init__()
33
+
34
+ @staticmethod
35
+ def _is_healthy() -> bool:
36
+ """Check if the service itself is healthy.
37
+
38
+ Returns:
39
+ bool: True if the service is healthy, False otherwise.
40
+ """
41
+ return True
42
+
43
+ def health(self) -> Health:
44
+ """Determine aggregate health of the system.
45
+
46
+ - Health exposed by implementations of BaseService in other
47
+ modules is automatically included into the health tree.
48
+ - See utils/_health.py:Health for an explanation of the health tree.
49
+
50
+ Returns:
51
+ Health: The aggregate health of the system.
52
+ """
53
+ components: dict[str, Health] = {}
54
+ for service_class in locate_subclasses(BaseService):
55
+ if service_class is not Service:
56
+ components[f"{service_class.__module__}.{service_class.__name__}"] = service_class().health()
57
+
58
+ # Set the system health status based on is_healthy attribute
59
+ status = Health.Code.UP if self._is_healthy() else Health.Code.DOWN
60
+ reason = None if self._is_healthy() else "System marked as unhealthy"
61
+ return Health(status=status, components=components, reason=reason)
62
+
63
+ @staticmethod
64
+ def info(include_environ: bool = False, filter_secrets: bool = True) -> dict[str, Any]:
65
+ """
66
+ Get info about configuration of service.
67
+
68
+ - Runtime information is automatically compiled.
69
+ - Settings are automatically aggregated from all implementations of
70
+ Pydantic BaseSettings in this package.
71
+ - Info exposed by implementations of BaseService in other modules is
72
+ automatically included into the info dict.
73
+
74
+ Returns:
75
+ dict[str, Any]: Service configuration.
76
+ """
77
+ rtn = {
78
+ "package": {
79
+ "version": __version__,
80
+ "name": __project_name__,
81
+ "repository": __repository_url__,
82
+ "local": __project_path__,
83
+ },
84
+ "runtime": {
85
+ "environment": __env__,
86
+ "python": {
87
+ "version": platform.python_version(),
88
+ "compiler": platform.python_compiler(),
89
+ "implementation": platform.python_implementation(),
90
+ },
91
+ "interpreter_path": sys.executable,
92
+ "command_line": " ".join(sys.argv),
93
+ "entry_point": sys.argv[0] if sys.argv else None,
94
+ "process_info": json.loads(get_process_info().model_dump_json()),
95
+ "username": pwd.getpwuid(os.getuid())[0],
96
+ "host": {
97
+ "system": platform.system(),
98
+ "release": platform.release(),
99
+ "version": platform.version(),
100
+ "machine": platform.machine(),
101
+ "processor": platform.processor(),
102
+ "hostname": platform.node(),
103
+ "ip_address": platform.uname().node,
104
+ "cpu_count": os.cpu_count(),
105
+ },
106
+ },
107
+ }
108
+
109
+ if include_environ:
110
+ if filter_secrets:
111
+ rtn["runtime"]["environ"] = {
112
+ k: v
113
+ for k, v in os.environ.items()
114
+ if not (
115
+ "token" in k.lower()
116
+ or "key" in k.lower()
117
+ or "secret" in k.lower()
118
+ or "password" in k.lower()
119
+ or "auth" in k.lower()
120
+ )
121
+ }
122
+ else:
123
+ rtn["runtime"]["environ"] = dict(os.environ)
124
+
125
+ settings = {}
126
+ for settings_class in locate_subclasses(BaseSettings):
127
+ settings_instance = load_settings(settings_class)
128
+ env_prefix = settings_instance.model_config.get("env_prefix", "")
129
+ settings_dict = json.loads(settings_instance.model_dump_json())
130
+ for key, value in settings_dict.items():
131
+ flat_key = f"{env_prefix}{key}".upper()
132
+ settings[flat_key] = value
133
+ rtn["settings"] = settings
134
+
135
+ for service_class in locate_subclasses(BaseService):
136
+ if service_class is not Service:
137
+ service = service_class()
138
+ rtn[service.key()] = service.info()
139
+
140
+ return rtn
141
+
142
+ @staticmethod
143
+ def div_by_zero() -> float:
144
+ """Divide by zero to trigger an error.
145
+
146
+ - This function is used to validate error handling and instrumentation
147
+ in the system.
148
+
149
+ Returns:
150
+ float: This function will raise a ZeroDivisionError before returning.
151
+ """
152
+ return 1 / 0
153
+
154
+ @staticmethod
155
+ def sleep(seconds: int) -> None:
156
+ """Sleep for a given number of seconds.
157
+
158
+ - This function is used to validate performance profiling in the system.
159
+
160
+ Args:
161
+ seconds (int): Number of seconds to sleep.
162
+ """
163
+ time.sleep(seconds)
@@ -0,0 +1,57 @@
1
+ """Utilities module."""
2
+
3
+ from ._api import VersionedAPIRouter
4
+ from ._cli import prepare_cli
5
+ from ._console import console
6
+ from ._constants import (
7
+ __author_email__,
8
+ __author_name__,
9
+ __documentation__url__,
10
+ __env__,
11
+ __env_file__,
12
+ __is_development_mode__,
13
+ __is_running_in_container__,
14
+ __project_name__,
15
+ __project_path__,
16
+ __repository_url__,
17
+ __version__,
18
+ )
19
+ from ._di import locate_implementations, locate_subclasses
20
+ from ._health import Health
21
+ from ._log import LogSettings, get_logger
22
+ from ._logfire import LogfireSettings
23
+ from ._process import ProcessInfo, get_process_info
24
+ from ._sentry import SentrySettings
25
+ from ._service import BaseService
26
+ from ._settings import load_settings
27
+ from .boot import boot
28
+
29
+ __all__ = [
30
+ "BaseService",
31
+ "Health",
32
+ "LogSettings",
33
+ "LogSettings",
34
+ "LogfireSettings",
35
+ "ProcessInfo",
36
+ "SentrySettings",
37
+ "VersionedAPIRouter",
38
+ "__author_email__",
39
+ "__author_name__",
40
+ "__documentation__url__",
41
+ "__env__",
42
+ "__env_file__",
43
+ "__is_development_mode__",
44
+ "__is_running_in_container__",
45
+ "__project_name__",
46
+ "__project_path__",
47
+ "__repository_url__",
48
+ "__version__",
49
+ "boot",
50
+ "console",
51
+ "get_logger",
52
+ "get_process_info",
53
+ "load_settings",
54
+ "locate_implementations",
55
+ "locate_subclasses",
56
+ "prepare_cli",
57
+ ]
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter
2
+
3
+
4
+ class VersionedAPIRouter(APIRouter):
5
+ """APIRouter with version attribute.
6
+
7
+ - Use this class to create versioned routers for your FastAPI application
8
+ that are automatically registered into the FastAPI app.
9
+ - The version attribute is used to identify the version of the API
10
+ that the router corresponds to.
11
+ - See constants.por versions defined for this system.
12
+ """
13
+
14
+ version: str
15
+
16
+ def __init__(self, version: str, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
17
+ super().__init__(*args, **kwargs)
18
+ self.version = version
@@ -0,0 +1,68 @@
1
+ """Command-line interface utilities."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from ._di import locate_implementations
9
+
10
+
11
+ def prepare_cli(cli: typer.Typer, epilog: str) -> None:
12
+ """
13
+ Dynamically locate, register and prepare subcommands.
14
+
15
+ Args:
16
+ cli (typer.Typer): Typer instance
17
+ epilog (str): Epilog to add
18
+ """
19
+ for _cli in locate_implementations(typer.Typer):
20
+ if _cli != cli:
21
+ cli.add_typer(_cli)
22
+
23
+ cli.info.epilog = epilog
24
+ cli.info.no_args_is_help = True
25
+ if not any(arg.endswith("typer") for arg in Path(sys.argv[0]).parts):
26
+ for command in cli.registered_commands:
27
+ command.epilog = cli.info.epilog
28
+
29
+ # add epilog for all subcommands
30
+ if not any(arg.endswith("typer") for arg in Path(sys.argv[0]).parts):
31
+ _add_epilog_recursively(cli, epilog)
32
+
33
+ # add no_args_is_help for all subcommands
34
+ _no_args_is_help_recursively(cli)
35
+
36
+
37
+ def _add_epilog_recursively(cli: typer.Typer, epilog: str) -> None:
38
+ """
39
+ Add epilog to all typers in the tree.
40
+
41
+ Args:
42
+ cli (typer.Typer): Typer instance
43
+ epilog (str): Epilog to add
44
+ """
45
+ cli.info.epilog = epilog
46
+ for group in cli.registered_groups:
47
+ if isinstance(group, typer.models.TyperInfo):
48
+ typer_instance = group.typer_instance
49
+ if (typer_instance is not cli) and typer_instance:
50
+ _add_epilog_recursively(typer_instance, epilog)
51
+ for command in cli.registered_commands:
52
+ if isinstance(command, typer.models.CommandInfo):
53
+ command.epilog = cli.info.epilog
54
+
55
+
56
+ def _no_args_is_help_recursively(cli: typer.Typer) -> None:
57
+ """
58
+ Show help if no command is given by the user.
59
+
60
+ Args:
61
+ cli (typer.Typer): Typer instance
62
+ """
63
+ for group in cli.registered_groups:
64
+ if isinstance(group, typer.models.TyperInfo):
65
+ group.no_args_is_help = True
66
+ typer_instance = group.typer_instance
67
+ if (typer_instance is not cli) and typer_instance:
68
+ _no_args_is_help_recursively(typer_instance)