Skip to content

Commit 3789622

Browse files
committed
Add dependency injection library
1 parent 8a8f889 commit 3789622

File tree

13 files changed

+334
-293
lines changed

13 files changed

+334
-293
lines changed

.github/workflows/pull_request.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ jobs:
2323
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
2424

2525
- name: Install uv
26-
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5
26+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
2727
with:
2828
version-file: pyproject.toml
2929
resolution-strategy: lowest
3030

3131
- name: Install Python
32-
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.0.1
32+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
3333
with:
3434
python-version-file: .python-version
3535

notebooks/notebook.ipynb

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,29 @@
55
"execution_count": null,
66
"metadata": {},
77
"outputs": [],
8-
"source": []
8+
"source": [
9+
"from python_template.api.main import configure_services\n",
10+
"from python_template.api.services.email_service import EmailService"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"service_provider = await configure_services().build_service_provider().__aenter__()"
20+
]
21+
},
22+
{
23+
"cell_type": "code",
24+
"execution_count": null,
25+
"metadata": {},
26+
"outputs": [],
27+
"source": [
28+
"email_service = await service_provider.get_required_service(EmailService)\n",
29+
"await email_service.send_email()"
30+
]
931
}
1032
],
1133
"metadata": {
@@ -24,7 +46,7 @@
2446
"name": "python",
2547
"nbconvert_exporter": "python",
2648
"pygments_lexer": "ipython3",
27-
"version": "3.13.7"
49+
"version": "3.14.0"
2850
}
2951
},
3052
"nbformat": 4,

pyproject.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ name = "python-template"
33
version = "0.1.0"
44
requires-python = ">=3.14"
55
dependencies = [
6-
"aiohttp>=3.13.2",
6+
"aiohttp>=3.13.3",
7+
"aspy-dependency-injection>=0.4.1",
78
"azure-cosmos>=4.14.3",
89
"azure-monitor-opentelemetry>=1.8.3",
9-
"fastapi[standard-no-fastapi-cloud-cli]>=0.127.0",
10+
"fastapi[standard-no-fastapi-cloud-cli]>=0.128.0",
1011
"pydantic>=2.12.5",
1112
"pydantic-settings[azure-key-vault]>=2.12.0",
1213
]
@@ -19,7 +20,7 @@ dev = [
1920
"pytest-cov>=7.0.0",
2021
"pytest-mock>=3.15.1",
2122
"ruff>=0.14.10",
22-
"ty>=0.0.5",
23+
"ty>=0.0.8",
2324
]
2425

2526
[build-system]
@@ -29,6 +30,9 @@ build-backend = "uv_build"
2930
[tool.uv]
3031
required-version = ">=0.9.16,<0.10.0"
3132

33+
[tool.ty.terminal]
34+
error-on-warning = true
35+
3236
[tool.ruff.lint]
3337
select = ["ALL"]
3438
ignore = [

scripts/script.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import asyncio
22

3+
from python_template.api.main import configure_services
4+
from python_template.api.services.email_service import EmailService
5+
36

47
async def main() -> None:
5-
pass
8+
async with configure_services().build_service_provider() as service_provider:
9+
email_service = await service_provider.get_required_service(EmailService)
10+
await email_service.send_email()
611

712

813
if __name__ == "__main__":

src/python_template/api/dependency_container.py

Lines changed: 0 additions & 114 deletions
This file was deleted.

src/python_template/api/main.py

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,108 @@
1+
import logging
12
from collections.abc import AsyncGenerator
23
from contextlib import asynccontextmanager
4+
from logging import Logger
35

6+
from aspy_dependency_injection.service_collection import ServiceCollection
7+
from azure.cosmos import PartitionKey
8+
from azure.cosmos.aio import CosmosClient, DatabaseProxy
9+
from azure.identity import DefaultAzureCredential
10+
from azure.monitor.opentelemetry import (
11+
configure_azure_monitor, # pyright: ignore[reportUnknownVariableType]
12+
)
413
from fastapi import FastAPI
514

6-
from python_template.api.dependency_container import DependencyContainer
15+
from python_template.api.application_settings import ApplicationSettings
16+
from python_template.api.services.email_service import EmailService
17+
from python_template.api.workflows.products.discontinue_product.discontinue_product_workflow import (
18+
DiscontinueProductWorkflow,
19+
)
720
from python_template.api.workflows.products.product_router import product_router
21+
from python_template.api.workflows.products.publish_product.publish_product_workflow import (
22+
PublishProductWorkflow,
23+
)
824
from python_template.common.application_environment import ApplicationEnvironment
25+
from python_template.domain.entities.product import Product
926

1027

11-
@asynccontextmanager
12-
async def lifespan(_: FastAPI) -> AsyncGenerator[None]:
13-
await DependencyContainer.initialize()
14-
yield
15-
await DependencyContainer.uninitialize()
28+
def inject_application_settings() -> ApplicationSettings:
29+
return ApplicationSettings() # ty:ignore[missing-argument]
1630

1731

18-
openapi_url = (
19-
"/openapi.json"
20-
if ApplicationEnvironment.get_current() != ApplicationEnvironment.PRODUCTION
21-
else None
22-
)
23-
app = FastAPI(
24-
openapi_url=openapi_url,
25-
lifespan=lifespan,
26-
)
27-
app.include_router(product_router)
32+
def inject_logging() -> Logger:
33+
return logging.getLogger(__name__)
34+
35+
36+
def inject_cosmos_client(
37+
application_settings: ApplicationSettings,
38+
) -> CosmosClient:
39+
return CosmosClient(
40+
url=application_settings.cosmos_db_no_sql_url,
41+
credential=application_settings.cosmos_db_no_sql_key.get_secret_value(),
42+
)
43+
44+
45+
def inject_cosmos_database(
46+
application_settings: ApplicationSettings, cosmos_client: CosmosClient
47+
) -> DatabaseProxy:
48+
return cosmos_client.get_database_client(
49+
application_settings.cosmos_db_no_sql_database
50+
)
51+
52+
53+
def configure_services() -> ServiceCollection:
54+
services = ServiceCollection()
55+
services.add_singleton(inject_application_settings)
56+
services.add_singleton(inject_logging)
57+
services.add_singleton(inject_cosmos_client)
58+
services.add_transient(inject_cosmos_database)
59+
services.add_transient(EmailService)
60+
services.add_transient(PublishProductWorkflow)
61+
services.add_transient(DiscontinueProductWorkflow)
62+
return services
63+
64+
65+
def create_app() -> FastAPI:
66+
@asynccontextmanager
67+
async def lifespan(_: FastAPI) -> AsyncGenerator[None]:
68+
async with configure_services().build_service_provider() as service_provider:
69+
application_settings = await service_provider.get_required_service(
70+
ApplicationSettings
71+
)
72+
logging.basicConfig(level=application_settings.logging_level)
73+
74+
if ApplicationEnvironment.get_current() != ApplicationEnvironment.LOCAL:
75+
configure_azure_monitor(
76+
connection_string=application_settings.application_insights_connection_string,
77+
credential=DefaultAzureCredential(),
78+
enable_live_metrics=True,
79+
)
80+
81+
if ApplicationEnvironment.get_current() == ApplicationEnvironment.LOCAL:
82+
cosmos_client = await service_provider.get_required_service(
83+
CosmosClient
84+
)
85+
await cosmos_client.create_database_if_not_exists(
86+
application_settings.cosmos_db_no_sql_database
87+
)
88+
cosmos_database = await service_provider.get_required_service(
89+
DatabaseProxy
90+
)
91+
await cosmos_database.create_container_if_not_exists(
92+
id=Product.__name__, partition_key=PartitionKey("/id")
93+
)
94+
yield
95+
96+
openapi_url = (
97+
"/openapi.json"
98+
if ApplicationEnvironment.get_current() != ApplicationEnvironment.PRODUCTION
99+
else None
100+
)
101+
app = FastAPI(openapi_url=openapi_url, lifespan=lifespan)
102+
app.include_router(product_router)
103+
return app
104+
105+
106+
app = create_app()
107+
services = configure_services()
108+
services.configure_fastapi(app)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class EmailService:
2+
async def send_email(self) -> None:
3+
pass
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
from typing import Annotated
2+
3+
from aspy_dependency_injection.annotations import Inject
14
from fastapi import APIRouter
25

3-
from python_template.api.dependency_container import DependencyContainer
46
from python_template.api.workflows.products.discontinue_product.discontinue_product_request import (
57
DiscontinueProductRequest,
68
)
9+
from python_template.api.workflows.products.discontinue_product.discontinue_product_workflow import (
10+
DiscontinueProductWorkflow,
11+
)
712
from python_template.api.workflows.products.publish_product.publish_product_request import (
813
PublishProductRequest,
914
)
1015
from python_template.api.workflows.products.publish_product.publish_product_response import (
1116
PublishProductResponse,
1217
)
18+
from python_template.api.workflows.products.publish_product.publish_product_workflow import (
19+
PublishProductWorkflow,
20+
)
1321

1422
product_router = APIRouter(
1523
prefix="/api/v1/products",
@@ -18,12 +26,16 @@
1826

1927

2028
@product_router.post("")
21-
async def publish_product(request: PublishProductRequest) -> PublishProductResponse:
22-
workflow = await DependencyContainer.get_publish_product_workflow()
29+
async def publish_product(
30+
request: PublishProductRequest,
31+
workflow: Annotated[PublishProductWorkflow, Inject()],
32+
) -> PublishProductResponse:
2333
return await workflow.execute(request)
2434

2535

2636
@product_router.post("/discontinue")
27-
async def discontinue_product(request: DiscontinueProductRequest) -> None:
28-
workflow = await DependencyContainer.get_discontinue_product_workflow()
37+
async def discontinue_product(
38+
request: DiscontinueProductRequest,
39+
workflow: Annotated[DiscontinueProductWorkflow, Inject()],
40+
) -> None:
2941
await workflow.execute(request)

0 commit comments

Comments
 (0)