Skip to content

Commit

Permalink
🗃️(api) add static data database schema
Browse files Browse the repository at this point in the history
This is the first draft implementation for static data persistency.

Note that we use Postgis extension for stations geolocalization.
  • Loading branch information
jmaupetit committed Apr 23, 2024
1 parent 93af361 commit c9880c9
Show file tree
Hide file tree
Showing 15 changed files with 981 additions and 91 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
services:
postgresql:
image: postgres:14
image: postgis/postgis:14-3.4
env:
POSTGRES_DB: test-qualicharge-api
POSTGRES_USER: qualicharge
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## [Unreleased]

### Added

- Implement static data database schemas

### Fixed

- Mark Static.id_pdc_itinerance field as required
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ stop: ## stop all servers
create-api-test-db: ## create API test database
@echo "Creating api service test database…"
@$(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."
@$(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."
.PHONY: create-api-test-db

drop-api-test-db: ## drop API test database
Expand All @@ -85,6 +86,7 @@ migrate-api: ## run alembic database migrations for the api service
@$(COMPOSE) up -d --wait postgresql
@echo "Creating api service database…"
@$(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."
@$(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."
@echo "Running migrations for api service…"
@bin/alembic upgrade head
.PHONY: migrate-api
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.8"

services:
postgresql:
image: postgres:14
image: postgis/postgis:14-3.4
env_file:
- env.d/postgresql
- env.d/api
Expand Down
3 changes: 2 additions & 1 deletion src/api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ alembic = "==1.13.1"
annotated-types = "==0.6.0"
email-validator = "==2.1.1"
fastapi = "==0.110.2"
geoalchemy2 = {extras = ["shapely"], version = "==0.14.7"}
httpx = "==0.27.0"
psycopg2-binary = "==2.9.9"
pydantic-extra-types = {extras = ["all"], version = "==2.6.0"}
Expand All @@ -19,6 +20,7 @@ uvicorn = {extras = ["standard"] }

[dev-packages]
black = "==24.4.0"
csvkit = "==1.5.0"
honcho = "==1.1.0"
mypy = "==1.9.0"
polyfactory = "==2.15.0"
Expand All @@ -27,7 +29,6 @@ pytest-cov = "==5.0.0"
pytest-httpx = "==0.30.0"
ruff = "==0.4.1"
types-python-jose = "==3.3.4.20240106"
csvkit = "==1.5.0"

[requires]
python_version = "3.12"
275 changes: 191 additions & 84 deletions src/api/Pipfile.lock

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions src/api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ name = "qualicharge"
version = "0.3.0"

# Third party packages configuration
[tool.coverage.run]
omit = [
"qualicharge/migrations/*"
]

[tool.pytest.ini_options]
addopts = "-v --cov-report term-missing --cov=qualicharge"
python_files = [
Expand All @@ -15,6 +20,11 @@ python_files = [
testpaths = [
"tests",
]
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
"migrations",
]

[tool.ruff.lint]
select = [
Expand Down Expand Up @@ -45,13 +55,22 @@ select = [
convention = "google"

[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.params.Depends", "fastapi.Query", "fastapi.params.Query"]
extend-immutable-calls = [
"fastapi.Depends",
"fastapi.params.Depends",
"fastapi.params.Query",
"fastapi.Query",
]

[tool.mypy]
plugins = "pydantic.mypy"
files = "./**/*.py"
exclude = ["/tests/"]
exclude = [
"qualicharge/migrations/"
]

[[tool.mypy.overrides]]
module = []
module = [
"shapely.*",
]
ignore_missing_imports = true
104 changes: 104 additions & 0 deletions src/api/qualicharge/factories/static.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,113 @@
"""QualiCharge static factories."""

from datetime import datetime, timedelta, timezone
from typing import Any, Callable, Dict, Generic, TypeVar
from uuid import uuid4

from faker import Faker
from geoalchemy2.types import Geometry
from polyfactory import Use
from polyfactory.factories.dataclass_factory import DataclassFactory
from polyfactory.factories.pydantic_factory import ModelFactory
from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory
from pydantic_extra_types.coordinate import Coordinate

from ..models.static import Statique
from ..schemas.static import (
Amenageur,
Enseigne,
Localisation,
Operateur,
PointDeCharge,
Station,
)

T = TypeVar("T")


class StatiqueFactory(ModelFactory[Statique]):
"""Statique model factory."""


class FrenchDataclassFactory(Generic[T], DataclassFactory[T]):
"""Dataclass factory using the french locale."""

__faker__ = Faker(locale="fr_FR")
__is_base_factory__ = True


class TimestampedSQLModelFactory(Generic[T], SQLAlchemyFactory[T]):
"""A base factory for timestamped SQLModel.
We expect SQLModel to define the following fields:
- id: UUID
- created_at: datetime
- updated_at: datetime
"""

__is_base_factory__ = True

id = Use(uuid4)
created_at = Use(lambda: datetime.now(timezone.utc) - timedelta(hours=1))
updated_at = Use(datetime.now, timezone.utc)


class AmenageurFactory(TimestampedSQLModelFactory[Amenageur]):
"""Amenageur schema factory."""

contact_amenageur = Use(FrenchDataclassFactory.__faker__.ascii_company_email)


class EnseigneFactory(TimestampedSQLModelFactory[Enseigne]):
"""Enseigne schema factory."""


class CoordinateFactory(DataclassFactory[Coordinate]):
"""Coordinate factory."""

longitude = Use(DataclassFactory.__faker__.pyfloat, min_value=-180, max_value=180)
latitude = Use(DataclassFactory.__faker__.pyfloat, min_value=-90, max_value=90)


class LocalisationFactory(TimestampedSQLModelFactory[Localisation]):
"""Localisation schema factory."""

@classmethod
def get_sqlalchemy_types(cls) -> Dict[Any, Callable[[], Any]]:
"""Add support for Geometry fields."""
types = super().get_sqlalchemy_types()
return {
Geometry: lambda: CoordinateFactory.build(),
**types,
}


class OperateurFactory(TimestampedSQLModelFactory[Operateur]):
"""Operateur schema factory."""

contact_operateur = Use(FrenchDataclassFactory.__faker__.ascii_company_email)
telephone_operateur = Use(FrenchDataclassFactory.__faker__.phone_number)


class PointDeChargeFactory(TimestampedSQLModelFactory[PointDeCharge]):
"""PointDeCharge schema factory."""

puissance_nominale = Use(
DataclassFactory.__faker__.pyfloat,
right_digits=2,
min_value=2.0,
max_value=100.0,
)


class StationFactory(TimestampedSQLModelFactory[Station]):
"""Station schema factory."""

date_maj = Use(DataclassFactory.__faker__.past_date)
date_mise_en_service = Use(DataclassFactory.__faker__.past_date)

amenageur_id = None
operateur_id = None
enseigne_id = None
localisation_id = None
15 changes: 14 additions & 1 deletion src/api/qualicharge/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@

from qualicharge.conf import settings

# Nota bene: be sure to import all models that need to be migrated here
from qualicharge.schemas.static import ( # noqa: F401
Amenageur,
Enseigne,
Localisation,
Operateur,
PointDeCharge,
Station,
)

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
Expand Down Expand Up @@ -70,7 +80,10 @@ def run_migrations_online() -> None:
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata,
)

with context.begin_transaction():
context.run_migrations()
Expand Down
Loading

0 comments on commit c9880c9

Please sign in to comment.