Skip to content

Commit 6fed8ab

Browse files
committed
Migrate to PostgreSQL
1 parent 8f4318f commit 6fed8ab

File tree

23 files changed

+248
-242
lines changed

23 files changed

+248
-242
lines changed

compose-dev.yaml

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
services:
2-
cosmosdb:
3-
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-EN20251205
2+
postgresql:
3+
image: postgres@sha256:fdd16e63f6c8ab50d422e4896b0d43d45fb72aa08bd33e4f5bda89bc4fbce98d # 18.1
44
restart: always
5+
environment:
6+
POSTGRES_USER: postgres
7+
POSTGRES_PASSWORD: postgres
8+
POSTGRES_DB: python_template
59
ports:
6-
- "8081:8081"
7-
- "1234:1234"
8-
9-
azurite:
10-
image: mcr.microsoft.com/azure-storage/azurite@sha256:647c63a91102a9d8e8000aab803436e1fc85fbb285e7ce830a82ee5d6661cf37 # 3.35.0
11-
restart: always
12-
ports:
13-
- "10000:10000"
14-
- "10001:10001"
15-
- "10002:10002"
16-
command: azurite --location /data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --disableTelemetry
10+
- "5432:5432"
1711
volumes:
18-
- azurite:/data
12+
- postgres-sql:/var/lib/postgresql
13+
- ./database:/docker-entrypoint-initdb.d:ro
1914

2015
volumes:
21-
azurite:
16+
postgres-sql:

database/0000001.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CREATE TABLE products(
2+
id UUID NOT NULL,
3+
name TEXT NOT NULL,
4+
description TEXT NULL,
5+
price DECIMAL NOT NULL,
6+
is_discontinued BOOLEAN NOT NULL,
7+
discontinuation_reason TEXT NULL,
8+
CONSTRAINT products_pkey PRIMARY KEY(id)
9+
);

notebooks/notebook.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"metadata": {},
77
"outputs": [],
88
"source": [
9-
"from python_template.api.main import configure_services\n",
9+
"from python_template.api.main import services\n",
1010
"from python_template.api.services.email_service import EmailService"
1111
]
1212
},
@@ -16,7 +16,7 @@
1616
"metadata": {},
1717
"outputs": [],
1818
"source": [
19-
"service_provider = await configure_services().build_service_provider().__aenter__()"
19+
"service_provider = await services.build_service_provider().__aenter__()"
2020
]
2121
},
2222
{

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ requires-python = ">=3.14"
55
dependencies = [
66
"aiohttp>=3.13.3",
77
"aspy-dependency-injection>=0.5.0",
8-
"azure-cosmos>=4.14.3",
8+
"asyncpg>=0.31.0",
99
"azure-monitor-opentelemetry>=1.8.3",
1010
"fastapi[standard-no-fastapi-cloud-cli]>=0.128.0",
11+
"greenlet>=3.3.0",
1112
"pydantic>=2.12.5",
1213
"pydantic-settings[azure-key-vault]>=2.12.0",
14+
"sqlmodel>=0.0.31",
1315
]
1416

1517
[dependency-groups]

scripts/script.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import asyncio
22

3-
from python_template.api.main import configure_services
3+
from python_template.api.main import services
44
from python_template.api.services.email_service import EmailService
55

66

77
async def main() -> None:
8-
async with configure_services().build_service_provider() as service_provider:
8+
async with services.build_service_provider() as service_provider:
99
email_service = await service_provider.get_required_service(EmailService)
1010
await email_service.send_email()
1111

src/python_template/api/.env.local

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
LOGGING_LEVEL="WARNING"
22
APPLICATION_INSIGHTS_CONNECTION_STRING="TBD"
3-
COSMOS_DB_NO_SQL_URL="http://localhost:8081"
4-
COSMOS_DB_NO_SQL_KEY="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
5-
COSMOS_DB_NO_SQL_DATABASE="mydatabase"
3+
POSTGRESQL_CONNECTION_STRING="postgresql+asyncpg://postgres:postgres@localhost/python_template"

src/python_template/api/application_settings.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from pathlib import Path
22

33
from azure.identity import DefaultAzureCredential
4-
from pydantic import SecretStr
54
from pydantic_settings import (
65
AzureKeyVaultSettingsSource,
76
BaseSettings,
@@ -17,9 +16,7 @@
1716
class ApplicationSettings(BaseSettings):
1817
logging_level: str
1918
application_insights_connection_string: str
20-
cosmos_db_no_sql_url: str
21-
cosmos_db_no_sql_key: SecretStr
22-
cosmos_db_no_sql_database: str
19+
postgresql_connection_string: str
2320

2421
model_config = SettingsConfigDict(
2522
extra="ignore",

src/python_template/api/main.py

Lines changed: 20 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
import logging
2-
from collections.abc import AsyncGenerator
3-
from contextlib import asynccontextmanager
4-
from logging import Logger
5-
61
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-
)
132
from fastapi import FastAPI
143

154
from python_template.api.application_settings import ApplicationSettings
5+
from python_template.api.service_collection_extensions import (
6+
add_observability,
7+
add_sqlmodel,
8+
)
169
from python_template.api.services.email_service import EmailService
1710
from python_template.api.workflows.products.discontinue_product.discontinue_product_workflow import (
1811
DiscontinueProductWorkflow,
@@ -22,87 +15,21 @@
2215
PublishProductWorkflow,
2316
)
2417
from python_template.common.application_environment import ApplicationEnvironment
25-
from python_template.domain.entities.product import Product
26-
27-
28-
def inject_application_settings() -> ApplicationSettings:
29-
return ApplicationSettings() # ty:ignore[missing-argument]
30-
31-
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-
4418

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()
19+
openapi_url = (
20+
"/openapi.json"
21+
if ApplicationEnvironment.get_current() != ApplicationEnvironment.PRODUCTION
22+
else None
23+
)
24+
app = FastAPI(openapi_url=openapi_url)
25+
app.include_router(product_router)
26+
27+
services = ServiceCollection()
28+
application_settings = ApplicationSettings() # ty:ignore[missing-argument]
29+
services.add_singleton(ApplicationSettings, application_settings)
30+
add_observability(services, application_settings)
31+
add_sqlmodel(services)
32+
services.add_transient(EmailService)
33+
services.add_transient(PublishProductWorkflow)
34+
services.add_transient(DiscontinueProductWorkflow)
10835
services.configure_fastapi(app)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import logging
2+
from logging import Logger
3+
4+
from aspy_dependency_injection.service_collection import ServiceCollection
5+
from azure.identity import DefaultAzureCredential
6+
from azure.monitor.opentelemetry import configure_azure_monitor
7+
from sqlalchemy.ext.asyncio import (
8+
AsyncEngine,
9+
AsyncSession,
10+
async_sessionmaker,
11+
create_async_engine,
12+
)
13+
14+
from python_template.api.application_settings import ApplicationSettings
15+
from python_template.common.application_environment import ApplicationEnvironment
16+
17+
18+
def add_observability(
19+
services: ServiceCollection, application_settings: ApplicationSettings
20+
) -> None:
21+
def inject_logging() -> Logger:
22+
return logging.getLogger(__name__)
23+
24+
logging.basicConfig(level=application_settings.logging_level)
25+
services.add_singleton(inject_logging)
26+
27+
if ApplicationEnvironment.get_current() != ApplicationEnvironment.LOCAL:
28+
configure_azure_monitor(
29+
connection_string=application_settings.application_insights_connection_string,
30+
credential=DefaultAzureCredential(),
31+
enable_live_metrics=True,
32+
)
33+
34+
35+
def add_sqlmodel(services: ServiceCollection) -> None:
36+
def inject_async_engine(application_settings: ApplicationSettings) -> AsyncEngine:
37+
return create_async_engine(application_settings.postgresql_connection_string)
38+
39+
def inject_async_sessionmaker(
40+
async_engine: AsyncEngine,
41+
) -> async_sessionmaker[AsyncSession]:
42+
return async_sessionmaker(
43+
async_engine, class_=AsyncSession, expire_on_commit=False
44+
)
45+
46+
def inject_async_session(
47+
async_sessionmaker: async_sessionmaker[AsyncSession],
48+
) -> AsyncSession:
49+
return async_sessionmaker()
50+
51+
services.add_singleton(inject_async_engine)
52+
services.add_singleton(inject_async_sessionmaker)
53+
services.add_transient(inject_async_session)
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from uuid import UUID
2+
13
from pydantic import BaseModel
24

35

46
class DiscontinueProductRequest(BaseModel):
5-
id: str
7+
id: UUID
68
discontinuation_reason: str | None = None

0 commit comments

Comments
 (0)