Skip to content

Commit c9880c9

Browse files
committed
🗃️(api) add static data database schema
This is the first draft implementation for static data persistency. Note that we use Postgis extension for stations geolocalization.
1 parent 93af361 commit c9880c9

File tree

15 files changed

+981
-91
lines changed

15 files changed

+981
-91
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
runs-on: ubuntu-latest
5757
services:
5858
postgresql:
59-
image: postgres:14
59+
image: postgis/postgis:14-3.4
6060
env:
6161
POSTGRES_DB: test-qualicharge-api
6262
POSTGRES_USER: qualicharge

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- Implement static data database schemas
14+
1115
### Fixed
1216

1317
- Mark Static.id_pdc_itinerance field as required

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ stop: ## stop all servers
6868
create-api-test-db: ## create API test database
6969
@echo "Creating api service test database…"
7070
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/postgres" -c "create database \"$${QUALICHARGE_TEST_DB_NAME}\";"' || echo "Duly noted, skipping database creation."
71+
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/$${QUALICHARGE_TEST_DB_NAME}" -c "create extension postgis;"' || echo "Duly noted, skipping extension creation."
7172
.PHONY: create-api-test-db
7273

7374
drop-api-test-db: ## drop API test database
@@ -85,6 +86,7 @@ migrate-api: ## run alembic database migrations for the api service
8586
@$(COMPOSE) up -d --wait postgresql
8687
@echo "Creating api service database…"
8788
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/postgres" -c "create database \"$${QUALICHARGE_DB_NAME}\";"' || echo "Duly noted, skipping database creation."
89+
@$(COMPOSE) exec postgresql bash -c 'psql "postgresql://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@$${QUALICHARGE_DB_HOST}:$${QUALICHARGE_DB_PORT}/$${QUALICHARGE_DB_NAME}" -c "create extension postgis;"' || echo "Duly noted, skipping extension creation."
8890
@echo "Running migrations for api service…"
8991
@bin/alembic upgrade head
9092
.PHONY: migrate-api

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: "3.8"
22

33
services:
44
postgresql:
5-
image: postgres:14
5+
image: postgis/postgis:14-3.4
66
env_file:
77
- env.d/postgresql
88
- env.d/api

src/api/Pipfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ alembic = "==1.13.1"
88
annotated-types = "==0.6.0"
99
email-validator = "==2.1.1"
1010
fastapi = "==0.110.2"
11+
geoalchemy2 = {extras = ["shapely"], version = "==0.14.7"}
1112
httpx = "==0.27.0"
1213
psycopg2-binary = "==2.9.9"
1314
pydantic-extra-types = {extras = ["all"], version = "==2.6.0"}
@@ -19,6 +20,7 @@ uvicorn = {extras = ["standard"] }
1920

2021
[dev-packages]
2122
black = "==24.4.0"
23+
csvkit = "==1.5.0"
2224
honcho = "==1.1.0"
2325
mypy = "==1.9.0"
2426
polyfactory = "==2.15.0"
@@ -27,7 +29,6 @@ pytest-cov = "==5.0.0"
2729
pytest-httpx = "==0.30.0"
2830
ruff = "==0.4.1"
2931
types-python-jose = "==3.3.4.20240106"
30-
csvkit = "==1.5.0"
3132

3233
[requires]
3334
python_version = "3.12"

src/api/Pipfile.lock

Lines changed: 191 additions & 84 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/pyproject.toml

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ name = "qualicharge"
66
version = "0.3.0"
77

88
# Third party packages configuration
9+
[tool.coverage.run]
10+
omit = [
11+
"qualicharge/migrations/*"
12+
]
13+
914
[tool.pytest.ini_options]
1015
addopts = "-v --cov-report term-missing --cov=qualicharge"
1116
python_files = [
@@ -15,6 +20,11 @@ python_files = [
1520
testpaths = [
1621
"tests",
1722
]
23+
[tool.ruff]
24+
# Exclude a variety of commonly ignored directories.
25+
exclude = [
26+
"migrations",
27+
]
1828

1929
[tool.ruff.lint]
2030
select = [
@@ -45,13 +55,22 @@ select = [
4555
convention = "google"
4656

4757
[tool.ruff.lint.flake8-bugbear]
48-
extend-immutable-calls = ["fastapi.Depends", "fastapi.params.Depends", "fastapi.Query", "fastapi.params.Query"]
58+
extend-immutable-calls = [
59+
"fastapi.Depends",
60+
"fastapi.params.Depends",
61+
"fastapi.params.Query",
62+
"fastapi.Query",
63+
]
4964

5065
[tool.mypy]
5166
plugins = "pydantic.mypy"
5267
files = "./**/*.py"
53-
exclude = ["/tests/"]
68+
exclude = [
69+
"qualicharge/migrations/"
70+
]
5471

5572
[[tool.mypy.overrides]]
56-
module = []
73+
module = [
74+
"shapely.*",
75+
]
5776
ignore_missing_imports = true
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,113 @@
11
"""QualiCharge static factories."""
22

3+
from datetime import datetime, timedelta, timezone
4+
from typing import Any, Callable, Dict, Generic, TypeVar
5+
from uuid import uuid4
6+
7+
from faker import Faker
8+
from geoalchemy2.types import Geometry
9+
from polyfactory import Use
10+
from polyfactory.factories.dataclass_factory import DataclassFactory
311
from polyfactory.factories.pydantic_factory import ModelFactory
12+
from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory
13+
from pydantic_extra_types.coordinate import Coordinate
414

515
from ..models.static import Statique
16+
from ..schemas.static import (
17+
Amenageur,
18+
Enseigne,
19+
Localisation,
20+
Operateur,
21+
PointDeCharge,
22+
Station,
23+
)
24+
25+
T = TypeVar("T")
626

727

828
class StatiqueFactory(ModelFactory[Statique]):
929
"""Statique model factory."""
30+
31+
32+
class FrenchDataclassFactory(Generic[T], DataclassFactory[T]):
33+
"""Dataclass factory using the french locale."""
34+
35+
__faker__ = Faker(locale="fr_FR")
36+
__is_base_factory__ = True
37+
38+
39+
class TimestampedSQLModelFactory(Generic[T], SQLAlchemyFactory[T]):
40+
"""A base factory for timestamped SQLModel.
41+
42+
We expect SQLModel to define the following fields:
43+
44+
- id: UUID
45+
- created_at: datetime
46+
- updated_at: datetime
47+
"""
48+
49+
__is_base_factory__ = True
50+
51+
id = Use(uuid4)
52+
created_at = Use(lambda: datetime.now(timezone.utc) - timedelta(hours=1))
53+
updated_at = Use(datetime.now, timezone.utc)
54+
55+
56+
class AmenageurFactory(TimestampedSQLModelFactory[Amenageur]):
57+
"""Amenageur schema factory."""
58+
59+
contact_amenageur = Use(FrenchDataclassFactory.__faker__.ascii_company_email)
60+
61+
62+
class EnseigneFactory(TimestampedSQLModelFactory[Enseigne]):
63+
"""Enseigne schema factory."""
64+
65+
66+
class CoordinateFactory(DataclassFactory[Coordinate]):
67+
"""Coordinate factory."""
68+
69+
longitude = Use(DataclassFactory.__faker__.pyfloat, min_value=-180, max_value=180)
70+
latitude = Use(DataclassFactory.__faker__.pyfloat, min_value=-90, max_value=90)
71+
72+
73+
class LocalisationFactory(TimestampedSQLModelFactory[Localisation]):
74+
"""Localisation schema factory."""
75+
76+
@classmethod
77+
def get_sqlalchemy_types(cls) -> Dict[Any, Callable[[], Any]]:
78+
"""Add support for Geometry fields."""
79+
types = super().get_sqlalchemy_types()
80+
return {
81+
Geometry: lambda: CoordinateFactory.build(),
82+
**types,
83+
}
84+
85+
86+
class OperateurFactory(TimestampedSQLModelFactory[Operateur]):
87+
"""Operateur schema factory."""
88+
89+
contact_operateur = Use(FrenchDataclassFactory.__faker__.ascii_company_email)
90+
telephone_operateur = Use(FrenchDataclassFactory.__faker__.phone_number)
91+
92+
93+
class PointDeChargeFactory(TimestampedSQLModelFactory[PointDeCharge]):
94+
"""PointDeCharge schema factory."""
95+
96+
puissance_nominale = Use(
97+
DataclassFactory.__faker__.pyfloat,
98+
right_digits=2,
99+
min_value=2.0,
100+
max_value=100.0,
101+
)
102+
103+
104+
class StationFactory(TimestampedSQLModelFactory[Station]):
105+
"""Station schema factory."""
106+
107+
date_maj = Use(DataclassFactory.__faker__.past_date)
108+
date_mise_en_service = Use(DataclassFactory.__faker__.past_date)
109+
110+
amenageur_id = None
111+
operateur_id = None
112+
enseigne_id = None
113+
localisation_id = None

src/api/qualicharge/migrations/env.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88

99
from qualicharge.conf import settings
1010

11+
# Nota bene: be sure to import all models that need to be migrated here
12+
from qualicharge.schemas.static import ( # noqa: F401
13+
Amenageur,
14+
Enseigne,
15+
Localisation,
16+
Operateur,
17+
PointDeCharge,
18+
Station,
19+
)
20+
1121
# this is the Alembic Config object, which provides
1222
# access to the values within the .ini file in use.
1323
config = context.config
@@ -70,7 +80,10 @@ def run_migrations_online() -> None:
7080
)
7181

7282
with connectable.connect() as connection:
73-
context.configure(connection=connection, target_metadata=target_metadata)
83+
context.configure(
84+
connection=connection,
85+
target_metadata=target_metadata,
86+
)
7487

7588
with context.begin_transaction():
7689
context.run_migrations()

0 commit comments

Comments
 (0)