feat(apps): cree les six tables data et l'hypertable readings
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""Création des six tables Data et de l'hypertable readings.
|
||||
|
||||
Revision ID: e6d2026091501
|
||||
Revises: 5353c0e4f094
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "e6d2026091501"
|
||||
down_revision = "5353c0e4f094"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"datasets",
|
||||
sa.Column("dataset_id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("dataset_name", sa.Text(), nullable=False),
|
||||
sa.Column("archive_sha256", sa.String(length=64), nullable=False),
|
||||
sa.Column("storage_uri", sa.Text(), nullable=False),
|
||||
sa.Column("source_timezone", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"metadata", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False
|
||||
),
|
||||
sa.CheckConstraint("dataset_id > 0", name="ck_datasets_positive_id"),
|
||||
sa.PrimaryKeyConstraint("dataset_id"),
|
||||
sa.UniqueConstraint("archive_sha256", name="uq_datasets_archive_sha256"),
|
||||
)
|
||||
op.create_table(
|
||||
"sites",
|
||||
sa.Column("site_id", sa.Text(), nullable=False),
|
||||
sa.Column("site_name", sa.Text(), nullable=False),
|
||||
sa.Column("site_type", sa.Text(), nullable=False),
|
||||
sa.Column("location", sa.Text(), nullable=True),
|
||||
sa.Column("capacity_kw", sa.Double(), nullable=True),
|
||||
sa.Column("status", sa.Text(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("site_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"predictions",
|
||||
sa.Column("prediction_id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("site_id", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("target_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("target_metric", sa.Text(), nullable=False),
|
||||
sa.Column("period_minutes", sa.Integer(), nullable=True),
|
||||
sa.Column("predicted_value", sa.Double(), nullable=True),
|
||||
sa.Column("model_reference", sa.Text(), nullable=False),
|
||||
sa.Column("status", sa.Text(), nullable=False),
|
||||
sa.Column("failure_reason", sa.Text(), nullable=True),
|
||||
sa.CheckConstraint(
|
||||
"(status = 'available' AND predicted_value IS NOT NULL AND failure_reason IS NULL) OR (status IN ('insufficient_data', 'error') AND predicted_value IS NULL AND failure_reason IS NOT NULL)",
|
||||
name="ck_predictions_status",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"target_metric <> 'consumption_kwh' OR period_minutes IS NOT NULL",
|
||||
name="ck_predictions_energy_period",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"target_metric IN ('consumption_kwh', 'consumption_kw')", name="ck_predictions_metric"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"period_minutes IS NULL OR period_minutes > 0", name="ck_predictions_period"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["site_id"], ["sites.site_id"], name="fk_predictions_site", ondelete="RESTRICT"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("prediction_id"),
|
||||
sa.UniqueConstraint("prediction_id", "site_id", name="uq_predictions_id_site"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_predictions_site_target", "predictions", ["site_id", "target_at"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"readings",
|
||||
sa.Column("reading_id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("site_id", sa.Text(), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("source", sa.Text(), nullable=False),
|
||||
sa.Column("dataset_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("consumption_kw", sa.Double(), nullable=True),
|
||||
sa.Column("consumption_kwh", sa.Double(), nullable=True),
|
||||
sa.Column("consumption_euros", sa.Numeric(precision=14, scale=2), nullable=True),
|
||||
sa.Column("voltage_v", sa.Double(), nullable=True),
|
||||
sa.Column("current_a", sa.Double(), nullable=True),
|
||||
sa.Column("power_factor", sa.Double(), nullable=True),
|
||||
sa.Column("temperature_celsius", sa.Double(), nullable=True),
|
||||
sa.Column("humidity_percent", sa.Double(), nullable=True),
|
||||
sa.Column("solar_irradiance_wm2", sa.Double(), nullable=True),
|
||||
sa.Column("is_working_hours", sa.Boolean(), nullable=True),
|
||||
sa.Column("data_quality", sa.Text(), nullable=True),
|
||||
sa.Column("null_reasons", postgresql.ARRAY(sa.Text()), nullable=True),
|
||||
sa.Column(
|
||||
"imputed_values", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=True
|
||||
),
|
||||
sa.Column("imputation_method", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"ingested_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"raw_data", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(source = 'csv' AND dataset_id IS NOT NULL) OR (source IN ('api_current', 'api_history') AND dataset_id IS NULL)",
|
||||
name="ck_readings_dataset_source",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"data_quality IS NULL OR data_quality IN ('good', 'partial', 'degraded', 'critical')",
|
||||
name="ck_readings_quality",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"source IN ('csv', 'api_current', 'api_history')", name="ck_readings_source"
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(imputed_values IS NULL AND imputation_method IS NULL) OR (imputed_values IS NOT NULL AND imputation_method IS NOT NULL)",
|
||||
name="ck_readings_imputation",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["dataset_id"], ["datasets.dataset_id"], name="fk_readings_dataset", ondelete="RESTRICT"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["site_id"], ["sites.site_id"], name="fk_readings_site", ondelete="RESTRICT"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("reading_id", "timestamp"),
|
||||
)
|
||||
op.create_index("ix_readings_dataset_id", "readings", ["dataset_id"], unique=False)
|
||||
op.create_index(
|
||||
"ix_readings_site_timestamp", "readings", ["site_id", "timestamp"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
"uq_readings_source",
|
||||
"readings",
|
||||
["site_id", "timestamp", "source", sa.literal_column("coalesce(dataset_id, 0)")],
|
||||
unique=True,
|
||||
)
|
||||
op.execute(
|
||||
"SELECT create_hypertable('readings', by_range('timestamp'), create_default_indexes => FALSE)"
|
||||
)
|
||||
op.create_table(
|
||||
"alerts",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("alert_id", sa.Text(), nullable=False),
|
||||
sa.Column("site_id", sa.Text(), nullable=False),
|
||||
sa.Column("source", sa.Text(), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("type", sa.Text(), nullable=False),
|
||||
sa.Column("severity", sa.Text(), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=False),
|
||||
sa.Column("value", sa.Double(), nullable=True),
|
||||
sa.Column("threshold", sa.Double(), nullable=True),
|
||||
sa.Column("metric", sa.Text(), nullable=True),
|
||||
sa.Column("prediction_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column(
|
||||
"raw_data", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=False
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"severity IN ('low', 'medium', 'high', 'critical')", name="ck_alerts_severity"
|
||||
),
|
||||
sa.CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alerts_source"),
|
||||
sa.CheckConstraint(
|
||||
"type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alerts_type"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["prediction_id", "site_id"],
|
||||
["predictions.prediction_id", "predictions.site_id"],
|
||||
name="fk_alerts_prediction_site",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["site_id"], ["sites.site_id"], name="fk_alerts_site", ondelete="RESTRICT"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("source", "site_id", "alert_id", name="uq_alerts_source_site_id"),
|
||||
)
|
||||
op.create_index("ix_alerts_site_timestamp", "alerts", ["site_id", "timestamp"], unique=False)
|
||||
op.create_table(
|
||||
"recommendations",
|
||||
sa.Column("recommendation_id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("alert_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("action", sa.Text(), nullable=False),
|
||||
sa.Column("explanation", sa.Text(), nullable=False),
|
||||
sa.Column("rule_reference", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["alert_id"], ["alerts.id"], name="fk_recommendations_alert", ondelete="RESTRICT"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("recommendation_id"),
|
||||
sa.UniqueConstraint("alert_id", "rule_reference", name="uq_recommendations_alert_rule"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("recommendations")
|
||||
op.drop_table("alerts")
|
||||
op.drop_table("readings")
|
||||
op.drop_table("predictions")
|
||||
op.drop_table("sites")
|
||||
op.drop_table("datasets")
|
||||
@@ -1,2 +1,6 @@
|
||||
# Piege : tout modele absent de ce module reste invisible de `alembic revision
|
||||
# --autogenerate`, qui genererait alors un drop de sa table.
|
||||
|
||||
from app.models.energy import Alert, Dataset, Prediction, Reading, Recommendation, Site
|
||||
|
||||
__all__ = ["Alert", "Dataset", "Prediction", "Reading", "Recommendation", "Site"]
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Tables du modèle de données EnerVision (CSV, API Mock et résultats ML)."""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
Double,
|
||||
ForeignKey,
|
||||
ForeignKeyConstraint,
|
||||
Index,
|
||||
Integer,
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Dataset(Base):
|
||||
__tablename__ = "datasets"
|
||||
__table_args__ = (
|
||||
CheckConstraint("dataset_id > 0", name="ck_datasets_positive_id"),
|
||||
UniqueConstraint("archive_sha256", name="uq_datasets_archive_sha256"),
|
||||
)
|
||||
|
||||
dataset_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
dataset_name: Mapped[str] = mapped_column(Text)
|
||||
archive_sha256: Mapped[str] = mapped_column(String(64))
|
||||
storage_uri: Mapped[str] = mapped_column(Text)
|
||||
source_timezone: Mapped[str | None] = mapped_column(Text)
|
||||
# "metadata" est réservé par SQLAlchemy ; le nom SQL reste inchangé.
|
||||
dataset_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB(none_as_null=True))
|
||||
|
||||
|
||||
class Site(Base):
|
||||
__tablename__ = "sites"
|
||||
|
||||
site_id: Mapped[str] = mapped_column(Text, primary_key=True)
|
||||
site_name: Mapped[str] = mapped_column(Text)
|
||||
site_type: Mapped[str] = mapped_column(Text)
|
||||
location: Mapped[str | None] = mapped_column(Text)
|
||||
capacity_kw: Mapped[float | None] = mapped_column(Double)
|
||||
status: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Reading(Base):
|
||||
__tablename__ = "readings"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"source IN ('csv', 'api_current', 'api_history')", name="ck_readings_source"
|
||||
),
|
||||
CheckConstraint(
|
||||
"(source = 'csv' AND dataset_id IS NOT NULL) OR "
|
||||
"(source IN ('api_current', 'api_history') AND dataset_id IS NULL)",
|
||||
name="ck_readings_dataset_source",
|
||||
),
|
||||
CheckConstraint(
|
||||
"data_quality IS NULL OR data_quality IN ('good', 'partial', 'degraded', 'critical')",
|
||||
name="ck_readings_quality",
|
||||
),
|
||||
CheckConstraint(
|
||||
"(imputed_values IS NULL AND imputation_method IS NULL) OR "
|
||||
"(imputed_values IS NOT NULL AND imputation_method IS NOT NULL)",
|
||||
name="ck_readings_imputation",
|
||||
),
|
||||
Index("ix_readings_site_timestamp", "site_id", "timestamp"),
|
||||
Index("ix_readings_dataset_id", "dataset_id"),
|
||||
)
|
||||
|
||||
reading_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
site_id: Mapped[str] = mapped_column(
|
||||
Text, ForeignKey("sites.site_id", name="fk_readings_site", ondelete="RESTRICT")
|
||||
)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True)
|
||||
source: Mapped[str] = mapped_column(Text)
|
||||
dataset_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger,
|
||||
ForeignKey("datasets.dataset_id", name="fk_readings_dataset", ondelete="RESTRICT"),
|
||||
)
|
||||
consumption_kw: Mapped[float | None] = mapped_column(Double)
|
||||
consumption_kwh: Mapped[float | None] = mapped_column(Double)
|
||||
consumption_euros: Mapped[Decimal | None] = mapped_column(Numeric(14, 2))
|
||||
voltage_v: Mapped[float | None] = mapped_column(Double)
|
||||
current_a: Mapped[float | None] = mapped_column(Double)
|
||||
power_factor: Mapped[float | None] = mapped_column(Double)
|
||||
temperature_celsius: Mapped[float | None] = mapped_column(Double)
|
||||
humidity_percent: Mapped[float | None] = mapped_column(Double)
|
||||
solar_irradiance_wm2: Mapped[float | None] = mapped_column(Double)
|
||||
is_working_hours: Mapped[bool | None] = mapped_column(Boolean)
|
||||
data_quality: Mapped[str | None] = mapped_column(Text)
|
||||
null_reasons: Mapped[list[str] | None] = mapped_column(ARRAY(Text))
|
||||
imputed_values: Mapped[dict[str, Any] | None] = mapped_column(JSONB(none_as_null=True))
|
||||
imputation_method: Mapped[str | None] = mapped_column(Text)
|
||||
ingested_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
raw_data: Mapped[dict[str, Any]] = mapped_column(JSONB(none_as_null=True))
|
||||
|
||||
|
||||
Index(
|
||||
"uq_readings_source",
|
||||
Reading.site_id,
|
||||
Reading.timestamp,
|
||||
Reading.source,
|
||||
func.coalesce(Reading.dataset_id, text("0")),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
|
||||
class Prediction(Base):
|
||||
__tablename__ = "predictions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("prediction_id", "site_id", name="uq_predictions_id_site"),
|
||||
Index("ix_predictions_site_target", "site_id", "target_at"),
|
||||
CheckConstraint(
|
||||
"target_metric IN ('consumption_kwh', 'consumption_kw')",
|
||||
name="ck_predictions_metric",
|
||||
),
|
||||
CheckConstraint(
|
||||
"period_minutes IS NULL OR period_minutes > 0", name="ck_predictions_period"
|
||||
),
|
||||
CheckConstraint(
|
||||
"target_metric <> 'consumption_kwh' OR period_minutes IS NOT NULL",
|
||||
name="ck_predictions_energy_period",
|
||||
),
|
||||
CheckConstraint(
|
||||
"(status = 'available' AND predicted_value IS NOT NULL AND failure_reason IS NULL) OR "
|
||||
"(status IN ('insufficient_data', 'error') AND predicted_value IS NULL "
|
||||
"AND failure_reason IS NOT NULL)",
|
||||
name="ck_predictions_status",
|
||||
),
|
||||
)
|
||||
|
||||
prediction_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
site_id: Mapped[str] = mapped_column(
|
||||
Text, ForeignKey("sites.site_id", name="fk_predictions_site", ondelete="RESTRICT")
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
target_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
target_metric: Mapped[str] = mapped_column(Text)
|
||||
period_minutes: Mapped[int | None] = mapped_column(Integer)
|
||||
predicted_value: Mapped[float | None] = mapped_column(Double)
|
||||
model_reference: Mapped[str] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(Text)
|
||||
failure_reason: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Alert(Base):
|
||||
__tablename__ = "alerts"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("source", "site_id", "alert_id", name="uq_alerts_source_site_id"),
|
||||
Index("ix_alerts_site_timestamp", "site_id", "timestamp"),
|
||||
ForeignKeyConstraint(
|
||||
["prediction_id", "site_id"],
|
||||
["predictions.prediction_id", "predictions.site_id"],
|
||||
name="fk_alerts_prediction_site",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
CheckConstraint("source IN ('api_mock', 'enervision')", name="ck_alerts_source"),
|
||||
CheckConstraint(
|
||||
"type IN ('spike', 'threshold', 'anomaly', 'outage', 'sensor')", name="ck_alerts_type"
|
||||
),
|
||||
CheckConstraint(
|
||||
"severity IN ('low', 'medium', 'high', 'critical')", name="ck_alerts_severity"
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
alert_id: Mapped[str] = mapped_column(Text)
|
||||
site_id: Mapped[str] = mapped_column(
|
||||
Text, ForeignKey("sites.site_id", name="fk_alerts_site", ondelete="RESTRICT")
|
||||
)
|
||||
source: Mapped[str] = mapped_column(Text)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
type: Mapped[str] = mapped_column(Text)
|
||||
severity: Mapped[str] = mapped_column(Text)
|
||||
message: Mapped[str] = mapped_column(Text)
|
||||
value: Mapped[float | None] = mapped_column(Double)
|
||||
threshold: Mapped[float | None] = mapped_column(Double)
|
||||
metric: Mapped[str | None] = mapped_column(Text)
|
||||
prediction_id: Mapped[int | None] = mapped_column(BigInteger)
|
||||
raw_data: Mapped[dict[str, Any]] = mapped_column(JSONB(none_as_null=True))
|
||||
|
||||
|
||||
class Recommendation(Base):
|
||||
__tablename__ = "recommendations"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("alert_id", "rule_reference", name="uq_recommendations_alert_rule"),
|
||||
)
|
||||
|
||||
recommendation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
alert_id: Mapped[int] = mapped_column(
|
||||
BigInteger, ForeignKey("alerts.id", name="fk_recommendations_alert", ondelete="RESTRICT")
|
||||
)
|
||||
action: Mapped[str] = mapped_column(Text)
|
||||
explanation: Mapped[str] = mapped_column(Text)
|
||||
rule_reference: Mapped[str] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
Reference in New Issue
Block a user