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.
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.4.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.4.dist-info/RECORD +0 -12
  36. {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/WHEEL +0 -0
  37. {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/entry_points.txt +0 -0
  38. {oe_python_template_example-0.3.4.dist-info → oe_python_template_example-0.3.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,20 +1,6 @@
1
- """Example project scaffolded and kept up to date with OE Python Template (oe-python-template)."""
1
+ """Copier template to scaffold Python projects compliant with best practices and modern tooling."""
2
2
 
3
- from .constants import (
4
- __project_name__,
5
- __project_path__,
6
- __version__,
7
- )
8
- from .models import Echo, Health, HealthStatus, Utterance
9
- from .service import Service
3
+ from .constants import MODULES_TO_INSTRUMENT
4
+ from .utils.boot import boot
10
5
 
11
- __all__ = [
12
- "Echo",
13
- "Health",
14
- "HealthStatus",
15
- "Service",
16
- "Utterance",
17
- "__project_name__",
18
- "__project_path__",
19
- "__version__",
20
- ]
6
+ boot(modules_to_instrument=MODULES_TO_INSTRUMENT)
@@ -1,44 +1,30 @@
1
1
  """Webservice API of OE Python Template Example.
2
2
 
3
- This module provides a webservice API with several endpoints:
4
- - A health/healthz endpoint that returns the health status of the service
5
- - A hello-world endpoint that returns a greeting message
6
- - An echo endpoint that echoes back the provided text
7
-
8
- The endpoints use Pydantic models for request and response validation.
3
+ - Provides a versioned API
4
+ - Automatically registers APIs of modules and mounts them to the main API.
9
5
  """
10
6
 
11
7
  import os
12
- from collections.abc import Generator
13
- from typing import Annotated
14
8
 
15
- from fastapi import Depends, FastAPI, Response, status
16
- from pydantic import BaseModel, Field
9
+ from fastapi import FastAPI
17
10
 
18
- from . import Echo, Health, HealthStatus, Service, Utterance
11
+ from .constants import API_VERSIONS
12
+ from .utils import (
13
+ VersionedAPIRouter,
14
+ __author_email__,
15
+ __author_name__,
16
+ __documentation__url__,
17
+ __repository_url__,
18
+ locate_implementations,
19
+ )
19
20
 
20
21
  TITLE = "OE Python Template Example"
21
- HELLO_WORLD_EXAMPLE = "Hello, world!"
22
22
  UVICORN_HOST = os.environ.get("UVICORN_HOST", "127.0.0.1")
23
23
  UVICORN_PORT = os.environ.get("UVICORN_PORT", "8000")
24
- CONTACT_NAME = "Helmut Hoffer von Ankershoffen"
25
- CONTACT_EMAIL = "helmuthva@gmail.com"
26
- CONTACT_URL = "https://github.com/helmut-hoffer-von-ankershoffen"
27
- TERMS_OF_SERVICE_URL = "https://oe-python-template-example.readthedocs.io/en/latest/"
28
-
29
-
30
- def get_service() -> Generator[Service, None, None]:
31
- """Get the service instance.
32
-
33
- Yields:
34
- Service: The service instance.
35
- """
36
- service = Service()
37
- try:
38
- yield service
39
- finally:
40
- # Cleanup code if needed
41
- pass
24
+ CONTACT_NAME = __author_name__
25
+ CONTACT_EMAIL = __author_email__
26
+ CONTACT_URL = __repository_url__
27
+ TERMS_OF_SERVICE_URL = __documentation__url__
42
28
 
43
29
 
44
30
  app = FastAPI(
@@ -52,130 +38,38 @@ app = FastAPI(
52
38
  terms_of_service=TERMS_OF_SERVICE_URL,
53
39
  openapi_tags=[
54
40
  {
55
- "name": "v1",
56
- "description": "API version 1, check link on the right",
41
+ "name": version,
42
+ "description": f"API version {version.lstrip('v')}, check link on the right",
57
43
  "externalDocs": {
58
44
  "description": "sub-docs",
59
- "url": f"http://{UVICORN_HOST}:{UVICORN_PORT}/api/v1/docs",
45
+ "url": f"http://{UVICORN_HOST}:{UVICORN_PORT}/api/{version}/docs",
60
46
  },
61
- },
62
- {
63
- "name": "v2",
64
- "description": "API version 2, check link on the right",
65
- "externalDocs": {
66
- "description": "sub-docs",
67
- "url": f"http://{UVICORN_HOST}:{UVICORN_PORT}/api/v2/docs",
68
- },
69
- },
47
+ }
48
+ for version, _ in API_VERSIONS.items()
70
49
  ],
71
50
  )
72
51
 
73
- api_v1 = FastAPI(
74
- version="1.0.0",
75
- title=TITLE,
76
- contact={
77
- "name": CONTACT_NAME,
78
- "email": CONTACT_EMAIL,
79
- "url": CONTACT_URL,
80
- },
81
- terms_of_service=TERMS_OF_SERVICE_URL,
82
- )
83
-
84
- api_v2 = FastAPI(
85
- version="2.0.0",
86
- title=TITLE,
87
- contact={
88
- "name": CONTACT_NAME,
89
- "email": CONTACT_EMAIL,
90
- "url": CONTACT_URL,
91
- },
92
- terms_of_service=TERMS_OF_SERVICE_URL,
93
- )
94
-
95
-
96
- @api_v1.get("/healthz", tags=["Observability"])
97
- @api_v1.get("/health", tags=["Observability"])
98
- @api_v2.get("/healthz", tags=["Observability"])
99
- @api_v2.get("/health", tags=["Observability"])
100
- async def health(service: Annotated[Service, Depends(get_service)], response: Response) -> Health:
101
- """Check the health of the service.
102
-
103
- This endpoint returns the health status of the service.
104
- The health status can be either UP or DOWN.
105
- If the service is healthy, the status will be UP.
106
- If the service is unhealthy, the status will be DOWN and a reason will be provided.
107
- The response will have a 200 OK status code if the service is healthy,
108
- and a 500 Internal Server Error status code if the service is unhealthy.
109
-
110
- Returns:
111
- Health: The health status of the service.
112
- """
113
- if service.healthy():
114
- health_result = Health(status=HealthStatus.UP)
115
- else:
116
- health_result = Health(status=HealthStatus.DOWN, reason="Service is unhealthy")
117
-
118
- if health_result.status == HealthStatus.DOWN:
119
- response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
120
-
121
- return health_result
122
-
123
-
124
- class _HelloWorldResponse(BaseModel):
125
- """Response model for hello-world endpoint."""
126
-
127
- message: str = Field(
128
- ...,
129
- description="The hello world message",
130
- examples=[HELLO_WORLD_EXAMPLE],
52
+ # Create API instances for each version
53
+ api_instances: dict["str", FastAPI] = {}
54
+ for version, semver in API_VERSIONS.items():
55
+ api_instances[version] = FastAPI(
56
+ version=semver,
57
+ title=TITLE,
58
+ contact={
59
+ "name": CONTACT_NAME,
60
+ "email": CONTACT_EMAIL,
61
+ "url": CONTACT_URL,
62
+ },
63
+ terms_of_service=TERMS_OF_SERVICE_URL,
131
64
  )
132
65
 
66
+ # Register routers with appropriate API versions
67
+ for router in locate_implementations(VersionedAPIRouter):
68
+ router_instance: VersionedAPIRouter = router
69
+ version = router_instance.version
70
+ if version in API_VERSIONS:
71
+ api_instances[version].include_router(router_instance)
133
72
 
134
- @api_v1.get("/hello-world", tags=["Basics"])
135
- @api_v2.get("/hello-world", tags=["Basics"])
136
- async def hello_world(service: Annotated[Service, Depends(get_service)]) -> _HelloWorldResponse:
137
- """
138
- Return a hello world message.
139
-
140
- Returns:
141
- _HelloWorldResponse: A response containing the hello world message.
142
- """
143
- return _HelloWorldResponse(message=service.get_hello_world())
144
-
145
-
146
- @api_v1.get("/echo/{text}", tags=["Basics"])
147
- async def echo(text: str) -> Echo:
148
- """
149
- Echo back the provided text.
150
-
151
- Args:
152
- text (str): The text to echo.
153
-
154
- Returns:
155
- Echo: The echo.
156
-
157
- Raises:
158
- 422 Unprocessable Entity: If text is not provided or empty.
159
- """
160
- return Service.echo(Utterance(text=text))
161
-
162
-
163
- @api_v2.post("/echo", tags=["Basics"])
164
- async def echo_v2(request: Utterance) -> Echo:
165
- """
166
- Echo back the provided utterance.
167
-
168
- Args:
169
- request (Utterance): The utterance to echo back.
170
-
171
- Returns:
172
- Echo: The echo.
173
-
174
- Raises:
175
- 422 Unprocessable Entity: If utterance is not provided or empty.
176
- """
177
- return Service.echo(request)
178
-
179
-
180
- app.mount("/v1", api_v1)
181
- app.mount("/v2", api_v2)
73
+ # Mount all API versions to the main app
74
+ for version in API_VERSIONS:
75
+ app.mount(f"/{version}", api_instances[version])
@@ -1,150 +1,22 @@
1
1
  """CLI (Command Line Interface) of OE Python Template Example."""
2
2
 
3
- import os
4
3
  import sys
5
- from enum import StrEnum
6
- from pathlib import Path
7
- from typing import Annotated
8
4
 
9
5
  import typer
10
- import uvicorn
11
- import yaml
12
- from rich.console import Console
13
6
 
14
- from . import Service, Utterance, __version__
15
- from .api import api_v1, api_v2
7
+ from .constants import MODULES_TO_INSTRUMENT
8
+ from .utils import __version__, boot, console, get_logger, prepare_cli
16
9
 
17
- cli = typer.Typer(help="Command Line Interface of OE Python Template Example")
18
- _service = Service()
19
- _console = Console()
10
+ boot(MODULES_TO_INSTRUMENT)
11
+ logger = get_logger(__name__)
20
12
 
13
+ cli = typer.Typer(help="Command Line Interface of ")
14
+ prepare_cli(cli, f"🧠 OE Python Template Example v{__version__} - built with love in Berlin 🐻")
21
15
 
22
- @cli.command()
23
- def health() -> None:
24
- """Indicate if service is healthy."""
25
- _console.print(_service.healthy())
26
-
27
-
28
- @cli.command()
29
- def info() -> None:
30
- """Print info about service configuration."""
31
- _console.print(_service.info())
32
-
33
-
34
- @cli.command()
35
- def echo(
36
- text: Annotated[
37
- str, typer.Argument(help="The text to echo")
38
- ] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
39
- json: Annotated[
40
- bool,
41
- typer.Option(
42
- help=("Print as JSON"),
43
- ),
44
- ] = False,
45
- ) -> None:
46
- """Echo the text."""
47
- echo = Service.echo(Utterance(text=text))
48
- if json:
49
- _console.print_json(data={"text": echo.text})
50
- else:
51
- _console.print(echo.text)
52
-
53
-
54
- @cli.command()
55
- def hello_world() -> None:
56
- """Print hello world message and what's in the environment variable THE_VAR."""
57
- _console.print(_service.get_hello_world())
58
-
59
-
60
- @cli.command()
61
- def serve(
62
- host: Annotated[str, typer.Option(help="Host to bind the server to")] = "127.0.0.1",
63
- port: Annotated[int, typer.Option(help="Port to bind the server to")] = 8000,
64
- watch: Annotated[bool, typer.Option(help="Enable auto-reload")] = True,
65
- ) -> None:
66
- """Start the API server."""
67
- _console.print(f"Starting API server at http://{host}:{port}")
68
- os.environ["UVICORN_HOST"] = host
69
- os.environ["UVICORN_PORT"] = str(port)
70
- uvicorn.run(
71
- "oe_python_template_example.api:app",
72
- host=host,
73
- port=port,
74
- reload=watch,
75
- )
76
-
77
-
78
- class APIVersion(StrEnum):
79
- """
80
- Enum representing the API versions.
81
-
82
- This enum defines the supported API verions:
83
- - V1: Output doc for v1 API
84
- - V2: Output doc for v2 API
85
-
86
- Usage:
87
- version = APIVersion.V1
88
- print(f"Using {version} version")
89
-
90
- """
91
-
92
- V1 = "v1"
93
- V2 = "v2"
94
-
95
-
96
- class OutputFormat(StrEnum):
97
- """
98
- Enum representing the supported output formats.
99
-
100
- This enum defines the possible formats for output data:
101
- - YAML: Output data in YAML format
102
- - JSON: Output data in JSON format
103
-
104
- Usage:
105
- format = OutputFormat.YAML
106
- print(f"Using {format} format")
107
- """
108
-
109
- YAML = "yaml"
110
- JSON = "json"
111
-
112
-
113
- @cli.command()
114
- def openapi(
115
- api_version: Annotated[APIVersion, typer.Option(help="API Version", case_sensitive=False)] = APIVersion.V1,
116
- output_format: Annotated[
117
- OutputFormat, typer.Option(help="Output format", case_sensitive=False)
118
- ] = OutputFormat.YAML,
119
- ) -> None:
120
- """Dump the OpenAPI specification to stdout (YAML by default)."""
121
- match api_version:
122
- case APIVersion.V1:
123
- schema = api_v1.openapi()
124
- case APIVersion.V2:
125
- schema = api_v2.openapi()
126
- match output_format:
127
- case OutputFormat.JSON:
128
- _console.print_json(data=schema)
129
- case OutputFormat.YAML:
130
- _console.print(yaml.dump(schema, width=80, default_flow_style=False), end="")
131
-
132
-
133
- def _apply_cli_settings(cli: typer.Typer, epilog: str) -> None:
134
- """Configure default behavior and add epilog to all typers in the tree."""
135
- cli.info.no_args_is_help = True
136
-
137
- if not any(arg.endswith("typer") for arg in Path(sys.argv[0]).parts):
138
- cli.info.epilog = epilog
139
- for command in cli.registered_commands:
140
- command.epilog = cli.info.epilog
141
-
142
-
143
- _apply_cli_settings(
144
- cli,
145
- f"🧠 OE Python Template Example v{__version__} - built with love in Berlin 🐻",
146
- )
147
-
148
-
149
- if __name__ == "__main__":
150
- cli()
16
+ if __name__ == "__main__": # pragma: no cover
17
+ try:
18
+ cli()
19
+ except Exception as e: # noqa: BLE001
20
+ logger.critical("Fatal error occurred: %s", e)
21
+ console.print(f"Fatal error occurred: {e}", style="error")
22
+ sys.exit(1)
@@ -1,10 +1,7 @@
1
- """Constants used throughout OE Python Template Example's codebase ."""
1
+ """Constants for the OE Python Template Example."""
2
2
 
3
- import importlib.metadata
4
- import pathlib
5
-
6
- __project_name__ = __name__.split(".")[0]
7
- __project_path__ = str(pathlib.Path(__file__).parent.parent.parent)
8
- __version__ = importlib.metadata.version(__project_name__)
9
-
10
- LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
3
+ API_VERSIONS = {
4
+ "v1": "1.0.0",
5
+ "v2": "2.0.0",
6
+ }
7
+ MODULES_TO_INSTRUMENT = ["oe_python_template_example.hello"]
@@ -0,0 +1,17 @@
1
+ """Hello module."""
2
+
3
+ from ._api import api_v1, api_v2
4
+ from ._cli import cli
5
+ from ._models import Echo, Utterance
6
+ from ._service import Service
7
+ from ._settings import Settings
8
+
9
+ __all__ = [
10
+ "Echo",
11
+ "Service",
12
+ "Settings",
13
+ "Utterance",
14
+ "api_v1",
15
+ "api_v2",
16
+ "cli",
17
+ ]
@@ -0,0 +1,94 @@
1
+ """Webservice API of Hello module.
2
+
3
+ This module provides a webservice API with several operations:
4
+ - A hello/world operation that returns a greeting message
5
+ - A hello/echo endpoint that echoes back the provided text
6
+ """
7
+
8
+ from collections.abc import Generator
9
+ from typing import Annotated
10
+
11
+ from fastapi import Depends
12
+ from pydantic import BaseModel, Field
13
+
14
+ from oe_python_template_example.utils import VersionedAPIRouter
15
+
16
+ from ._models import Echo, Utterance
17
+ from ._service import Service
18
+
19
+ HELLO_WORLD_EXAMPLE = "Hello, world!"
20
+
21
+ # VersionedAPIRouters exported by modules via their __init__.py are automatically registered
22
+ # and injected into the main API app, see ../api.py.
23
+ api_v1 = VersionedAPIRouter("v1", prefix="/hello", tags=["hello"])
24
+ api_v2 = VersionedAPIRouter("v2", prefix="/hello", tags=["hello"])
25
+
26
+
27
+ def get_service() -> Generator[Service, None, None]:
28
+ """Get instance of Service.
29
+
30
+ Yields:
31
+ Service: The service instance.
32
+ """
33
+ service = Service()
34
+ try:
35
+ yield service
36
+ finally:
37
+ # Cleanup code if needed
38
+ pass
39
+
40
+
41
+ class _HelloWorldResponse(BaseModel):
42
+ """Response model for hello-world endpoint."""
43
+
44
+ message: str = Field(
45
+ ...,
46
+ description="The hello world message",
47
+ examples=[HELLO_WORLD_EXAMPLE],
48
+ )
49
+
50
+
51
+ @api_v1.get("/world")
52
+ @api_v2.get("/world")
53
+ def hello_world(service: Annotated[Service, Depends(get_service)]) -> _HelloWorldResponse:
54
+ """
55
+ Return a hello world message.
56
+
57
+ Returns:
58
+ _HelloWorldResponse: A response containing the hello world message.
59
+ """
60
+ return _HelloWorldResponse(message=service.get_hello_world())
61
+
62
+
63
+ @api_v1.get("/echo/{text}")
64
+ def echo(text: str) -> Echo:
65
+ """
66
+ Echo back the provided text.
67
+
68
+ Args:
69
+ text (str): The text to echo.
70
+
71
+ Returns:
72
+ Echo: The echo.
73
+
74
+ Raises:
75
+ 422 Unprocessable Entity: If text is not provided or empty.
76
+ """
77
+ return Service.echo(Utterance(text=text))
78
+
79
+
80
+ @api_v2.post("/echo")
81
+ def echo_v2(request: Utterance) -> Echo:
82
+ """
83
+ Echo back the provided utterance.
84
+
85
+ Args:
86
+ request (Utterance): The utterance to echo back.
87
+
88
+ Returns:
89
+ Echo: The echo.
90
+
91
+ Raises:
92
+ 422 Unprocessable Entity: If utterance is not provided or empty.
93
+ """
94
+ return Service.echo(request)
@@ -0,0 +1,47 @@
1
+ """CLI (Command Line Interface) of OE Python Template Example."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from oe_python_template_example.utils import console, get_logger
8
+
9
+ from ._models import Utterance
10
+ from ._service import Service
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ # CLI apps exported by modules via their __init__.py are automatically registered and injected into the main CLI app
15
+ cli = typer.Typer(name="hello", help="Hello commands")
16
+ _service = Service()
17
+
18
+
19
+ @cli.command()
20
+ def echo(
21
+ text: Annotated[
22
+ str, typer.Argument(help="The text to echo")
23
+ ] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
24
+ json: Annotated[
25
+ bool,
26
+ typer.Option(
27
+ help=("Print as JSON"),
28
+ ),
29
+ ] = False,
30
+ ) -> None:
31
+ """Echo the text.
32
+
33
+ Args:
34
+ text (str): The text to echo.
35
+ json (bool): Print as JSON.
36
+ """
37
+ echo = Service.echo(Utterance(text=text))
38
+ if json:
39
+ console.print_json(data={"text": echo.text})
40
+ else:
41
+ console.print(echo.text)
42
+
43
+
44
+ @cli.command()
45
+ def world() -> None:
46
+ """Print hello world message and what's in the environment variable THE_VAR."""
47
+ console.print(_service.get_hello_world())
@@ -0,0 +1,4 @@
1
+ """Constants of the hello module."""
2
+
3
+ HELLO_WORLD_EN_US = "Hello, world!"
4
+ HELLO_WORLD_DE_DE = "Hallo, Welt!"
@@ -0,0 +1,28 @@
1
+ """Models of the hello module."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ _UTTERANCE_EXAMPLE = "Hello, world!"
6
+ _ECHO_EXAMPLE = "HELLO, WORLD!"
7
+
8
+
9
+ class Utterance(BaseModel):
10
+ """Model representing a text utterance."""
11
+
12
+ text: str = Field(
13
+ ...,
14
+ min_length=1,
15
+ description="The utterance to echo back",
16
+ examples=[_UTTERANCE_EXAMPLE],
17
+ )
18
+
19
+
20
+ class Echo(BaseModel):
21
+ """Response model for echo endpoint."""
22
+
23
+ text: str = Field(
24
+ ...,
25
+ min_length=1,
26
+ description="The echo",
27
+ examples=[_ECHO_EXAMPLE],
28
+ )