From 6533fbbec31a812f0f41ba6462717319c209af2a Mon Sep 17 00:00:00 2001 From: ronan Date: Wed, 16 Jul 2025 10:29:30 +0200 Subject: [PATCH 1/9] small refactor of the slack notification --- src/app/services/slack.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/app/services/slack.py b/src/app/services/slack.py index 62e14db2..f19b71b2 100644 --- a/src/app/services/slack.py +++ b/src/app/services/slack.py @@ -5,6 +5,8 @@ import json import logging +from datetime import datetime +from zoneinfo import ZoneInfo import requests @@ -42,6 +44,11 @@ def notify(self, slack_hook: str, message_detection: str, url: str, camera_name: raise ValueError("Invalid JSON format for message_detection") from e azimuth = detection_data.get("azimuth", "Inconnu") + created_at_str = detection_data.get("created_at", "Inconnu") + utc_dt = datetime.fromisoformat(created_at_str) + utc_dt = utc_dt.replace(tzinfo=ZoneInfo("UTC")) + paris_dt = utc_dt.astimezone(ZoneInfo("Europe/Paris")) + if url is not None: message = { "text": "Un feu a été detecté !", @@ -51,10 +58,13 @@ def notify(self, slack_hook: str, message_detection: str, url: str, camera_name: "block_id": "section567", "text": { "type": "mrkdwn", - "text": ":fire: \n\n Nom du site concerné : " + "text": ":date: " + + paris_dt.strftime("%Y-%m-%d %H:%M:%S") + + "\n Nom du site concerné : " + camera_name - + "\n Azimuth :" + + "\n Azimuth de detection : " + str(azimuth) + + "°" + "\n https://platform.pyronear.org/", }, }, @@ -70,10 +80,13 @@ def notify(self, slack_hook: str, message_detection: str, url: str, camera_name: "block_id": "section567", "text": { "type": "mrkdwn", - "text": ":fire: \n\n Nom du site concerné : " + "text": ":date: " + + paris_dt.strftime("%Y-%m-%d %H:%M:%S") + + "\n Nom du site concerné : " + camera_name - + "\n Azimuth :" + + "\n Azimuth de detection : " + str(azimuth) + + "°" + "\n https://platform.pyronear.org/", }, }, From 3b67976eecb9bda3ede77b21a531f03c02c5466f Mon Sep 17 00:00:00 2001 From: ronan Date: Wed, 16 Jul 2025 11:03:56 +0200 Subject: [PATCH 2/9] add url of the S3 in slack notification --- src/app/services/slack.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/services/slack.py b/src/app/services/slack.py index f19b71b2..37e1b052 100644 --- a/src/app/services/slack.py +++ b/src/app/services/slack.py @@ -65,7 +65,9 @@ def notify(self, slack_hook: str, message_detection: str, url: str, camera_name: + "\n Azimuth de detection : " + str(azimuth) + "°" - + "\n https://platform.pyronear.org/", + + "\n https://platform.pyronear.org/" + + "\n " + + url, }, }, {"type": "image", "image_url": url, "alt_text": "Haunted hotel image"}, From 5568a0d248147fbeb65ced1ae7a4d22ac5d4df27 Mon Sep 17 00:00:00 2001 From: ronan Date: Wed, 20 Aug 2025 22:55:50 +0200 Subject: [PATCH 3/9] fix migration files --- scripts/dbdiagram.txt | 2 +- scripts/test_e2e.py | 2 +- src/app/models.py | 25 +++++++- src/app/schemas/detections.py | 4 +- src/app/schemas/sequences.py | 4 +- ...5_06_20_1945-42dzeg392dhu_fix_migration.py | 54 ++++++++++++++++++ ..._06_25_1720-2853acd1fc32_add_slack_hook.py | 25 ++++++++ ...7-307a1d6d490d_modify_is_wilfire_column.py | 57 +++++++++++++++++++ src/tests/conftest.py | 2 +- src/tests/endpoints/test_sequences.py | 20 +++---- 10 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py create mode 100644 src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py create mode 100644 src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py diff --git a/scripts/dbdiagram.txt b/scripts/dbdiagram.txt index 6d2af5d2..94ac99ee 100644 --- a/scripts/dbdiagram.txt +++ b/scripts/dbdiagram.txt @@ -37,7 +37,7 @@ Table "Sequence" as S { "id" int [not null] "camera_id" int [ref: > C.id, not null] "azimuth" float [not null] - "is_wildfire" bool + "is_wildfire" AnnotationType "started_at" timestamp [not null] "last_seen_at" timestamp [not null] Indexes { diff --git a/scripts/test_e2e.py b/scripts/test_e2e.py index f705327d..053e9e80 100644 --- a/scripts/test_e2e.py +++ b/scripts/test_e2e.py @@ -152,7 +152,7 @@ def main(args): == 1 ) # Label the sequence - api_request("patch", f"{args.endpoint}/sequences/{sequence['id']}/label", agent_auth, {"is_wildfire": True}) + api_request("patch", f"{args.endpoint}/sequences/{sequence['id']}/label", agent_auth, {"is_wildfire": "wildfire_smoke"}) # Check the sequence's detections dets = api_request("get", f"{args.endpoint}/sequences/{sequence['id']}/detections", agent_auth) assert len(dets) == 3 diff --git a/src/app/models.py b/src/app/models.py index 455f5c8c..de7423a1 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -27,6 +27,29 @@ class Role(str, Enum): USER = "user" +class AnnotationType(str, Enum): + WILDFIRE_SMOKE = "wildfire_smoke" + OTHER_SMOKE = "other_smoke" + ANTENNA = "antenna" + BUILDING = "building" + CLIFF = "cliff" + DARK = "dark" + DUST = "dust" + HIGH_CLOUD = "high_cloud" + LOW_CLOUD = "low_cloud" + LENS_FLARE = "lens_flare" + LENS_DROPLET = "lens_droplet" + LIGHT = "light" + RAIN = "rain" + TRAIL = "trail" + ROAD = "road" + SKY = "sky" + TREE = "tree" + WATER_BODY = "water_body" + DOUBT = "doubt" + OTHER = "other" + + class User(SQLModel, table=True): __tablename__ = "users" id: int = Field(None, primary_key=True) @@ -69,7 +92,7 @@ class Sequence(SQLModel, table=True): id: int = Field(None, primary_key=True) camera_id: int = Field(..., foreign_key="cameras.id", nullable=False) azimuth: float = Field(..., ge=0, lt=360) - is_wildfire: Union[bool, None] = None + is_wildfire: Union[AnnotationType, None] = None started_at: datetime = Field(..., nullable=False) last_seen_at: datetime = Field(..., nullable=False) diff --git a/src/app/schemas/detections.py b/src/app/schemas/detections.py index 6bb9a32d..a15a1d45 100644 --- a/src/app/schemas/detections.py +++ b/src/app/schemas/detections.py @@ -9,13 +9,13 @@ from pydantic import BaseModel, Field from app.core.config import settings -from app.models import Detection +from app.models import Detection, AnnotationType __all__ = ["Azimuth", "DetectionCreate", "DetectionLabel", "DetectionUrl"] class DetectionLabel(BaseModel): - is_wildfire: bool + is_wildfire: AnnotationType class Azimuth(BaseModel): diff --git a/src/app/schemas/sequences.py b/src/app/schemas/sequences.py index 635c83d4..d51bc0cf 100644 --- a/src/app/schemas/sequences.py +++ b/src/app/schemas/sequences.py @@ -7,7 +7,7 @@ from pydantic import BaseModel -from app.models import Sequence +from app.models import Sequence, AnnotationType __all__ = ["SequenceUpdate", "SequenceWithCone"] @@ -18,7 +18,7 @@ class SequenceUpdate(BaseModel): class SequenceLabel(BaseModel): - is_wildfire: bool + is_wildfire: AnnotationType class SequenceWithCone(Sequence): diff --git a/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py b/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py new file mode 100644 index 00000000..5e0aa0a6 --- /dev/null +++ b/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py @@ -0,0 +1,54 @@ +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# Ajoute ton identifiant de révision et dépendance si besoin +revision = '42dzeg392dhu' +down_revision = '4265426f8438' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. Créer la table sequences (doit précéder la FK) + op.create_table( + "sequences", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("camera_id", sa.Integer(), sa.ForeignKey("camera.id"), nullable=False), + sa.Column("azimuth", sa.Float(), nullable=False), + sa.Column("is_wildfire", sa.Boolean(), nullable=True), # sera modifié par la 4e migration + sa.Column("started_at", sa.DateTime(), nullable=False), + sa.Column("last_seen_at", sa.DateTime(), nullable=False), + ) + + # 2. Ajouter les colonnes manquantes + op.add_column("camera", sa.Column("last_image", sa.String(), nullable=True)) + op.add_column("organization", sa.Column("telegram_id", sa.String(), nullable=True)) + op.add_column("detection", sa.Column("sequence_id", sa.Integer(), nullable=True)) + op.add_column("detection", sa.Column("bboxes", sa.String(length=5000), nullable=False)) # adapter à settings + + # 3. Ajouter la contrainte FK après la création de la table sequences + op.create_foreign_key( + "fk_detection_sequence", + "detection", + "sequences", + ["sequence_id"], + ["id"], + ) + + # 4. Créer la table webhooks + op.create_table( + "webhooks", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("url", sa.String(), nullable=False, unique=True), + ) + + +def downgrade() -> None: + op.drop_table("webhooks") + op.drop_constraint("fk_detection_sequence", "detection", type_="foreignkey") + op.drop_column("detection", "bboxes") + op.drop_column("detection", "sequence_id") + op.drop_column("organization", "telegram_id") + op.drop_column("camera", "last_image") + op.drop_table("sequences") \ No newline at end of file diff --git a/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py b/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py new file mode 100644 index 00000000..36bc01a4 --- /dev/null +++ b/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py @@ -0,0 +1,25 @@ +"""Add Slack Hook +Revision ID: 2853acd1fc32 +Revises: 4265426f8438 +Create Date: 2025-06-25 17:20:14.959429 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +import sqlmodel +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "2853acd1fc32" +down_revision: Union[str, None] = "42dzeg392dhu" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("organization", sa.Column("slack_hook", sqlmodel.sql.sqltypes.AutoString(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("organization", "slack_hook") \ No newline at end of file diff --git a/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py b/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py new file mode 100644 index 00000000..4ec19a8b --- /dev/null +++ b/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py @@ -0,0 +1,57 @@ +"""modify is_wilfire column + +Revision ID: 307a1d6d490d +Revises: 2853acd1fc32 +Create Date: 2025-08-20 16:47:05.346210 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '307a1d6d490d' +down_revision: Union[str, None] = '2853acd1fc32' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +# Define the new ENUM type +annotation_type_enum = sa.Enum( + "WILDFIRE_SMOKE", "OTHER_SMOKE", "ANTENNA", "BUILDING", "CLIFF", "DARK", + "DUST", "HIGH_CLOUD", "LOW_CLOUD", "LENS_FLARE", "LENS_DROPLET", "LIGHT", + "RAIN", "TRAIL", "ROAD", "SKY", "TREE", "WATER_BODY", "DOUBT", "OTHER", + name="annotationtype" +) + +def upgrade(): + # Create the enum type in the database + annotation_type_enum.create(op.get_bind(), checkfirst=True) + + # Use raw SQL with a CASE expression for the conversion + op.execute(""" + ALTER TABLE sequences + ALTER COLUMN is_wildfire + TYPE annotationtype + USING CASE + WHEN is_wildfire = TRUE THEN 'WILDFIRE_SMOKE'::annotationtype + ELSE 'OTHER'::annotationtype + END + """) + +def downgrade(): + # Revert the column back to a boolean (or previous enum if applicable) + op.execute(""" + ALTER TABLE sequences + ALTER COLUMN is_wildfire + TYPE boolean + USING CASE + WHEN is_wildfire = 'WILDFIRE_SMOKE' THEN TRUE + ELSE FALSE + END + """) + + # Drop the enum type from the DB + annotation_type_enum.drop(op.get_bind(), checkfirst=True) \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 56f950c5..ed02ddcc 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -138,7 +138,7 @@ "id": 1, "camera_id": 1, "azimuth": 43.7, - "is_wildfire": True, + "is_wildfire": "wildfire_smoke", "started_at": datetime.strptime("2023-11-07T15:08:19.226673", dt_format), "last_seen_at": datetime.strptime("2023-11-07T15:28:19.226673", dt_format), }, diff --git a/src/tests/endpoints/test_sequences.py b/src/tests/endpoints/test_sequences.py index 795f5f56..bf9e8846 100644 --- a/src/tests/endpoints/test_sequences.py +++ b/src/tests/endpoints/test_sequences.py @@ -94,18 +94,18 @@ async def test_delete_sequence( @pytest.mark.parametrize( ("user_idx", "sequence_id", "payload", "status_code", "status_detail", "expected_idx"), [ - (None, 1, {"is_wildfire": True}, 401, "Not authenticated", None), - (0, 0, {"is_wildfire": True}, 422, None, None), - (0, 99, {"is_wildfire": True}, 404, None, None), - (0, 1, {"label": True}, 422, None, None), + (None, 1, {"is_wildfire": "wildfire_smoke"}, 401, "Not authenticated", None), + (0, 0, {"is_wildfire": "wildfire_smoke"}, 422, None, None), + (0, 99, {"is_wildfire": "wildfire_smoke"}, 404, None, None), + (0, 1, {"label": "wildfire_smoke"}, 422, None, None), (0, 1, {"is_wildfire": "hello"}, 422, None, None), # (0, 1, {"is_wildfire": "True"}, 422, None, None), # odd, this works - (0, 1, {"is_wildfire": True}, 200, None, 0), - (0, 2, {"is_wildfire": True}, 200, None, 1), - (1, 1, {"is_wildfire": True}, 200, None, 0), - (1, 2, {"is_wildfire": True}, 403, None, None), - (2, 1, {"is_wildfire": True}, 403, None, None), - (2, 2, {"is_wildfire": True}, 403, None, None), # User cannot label + (0, 1, {"is_wildfire": "wildfire_smoke"}, 200, None, 0), + (0, 2, {"is_wildfire": "wildfire_smoke"}, 200, None, 1), + (1, 1, {"is_wildfire": "wildfire_smoke"}, 200, None, 0), + (1, 2, {"is_wildfire": "wildfire_smoke"}, 403, None, None), + (2, 1, {"is_wildfire": "wildfire_smoke"}, 403, None, None), + (2, 2, {"is_wildfire": "wildfire_smoke"}, 403, None, None), # User cannot label ], ) @pytest.mark.asyncio From 82af8f181279502e5fcf116b2e2a5a1f410615e2 Mon Sep 17 00:00:00 2001 From: ronan Date: Wed, 20 Aug 2025 22:58:50 +0200 Subject: [PATCH 4/9] run quality --- scripts/test_e2e.py | 4 +- src/app/schemas/detections.py | 2 +- src/app/schemas/sequences.py | 2 +- ...5_06_20_1945-42dzeg392dhu_fix_migration.py | 9 ++--- ..._06_25_1720-2853acd1fc32_add_slack_hook.py | 2 +- ...7-307a1d6d490d_modify_is_wilfire_column.py | 38 ++++++++++++++----- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/scripts/test_e2e.py b/scripts/test_e2e.py index 053e9e80..74aea706 100644 --- a/scripts/test_e2e.py +++ b/scripts/test_e2e.py @@ -152,7 +152,9 @@ def main(args): == 1 ) # Label the sequence - api_request("patch", f"{args.endpoint}/sequences/{sequence['id']}/label", agent_auth, {"is_wildfire": "wildfire_smoke"}) + api_request( + "patch", f"{args.endpoint}/sequences/{sequence['id']}/label", agent_auth, {"is_wildfire": "wildfire_smoke"} + ) # Check the sequence's detections dets = api_request("get", f"{args.endpoint}/sequences/{sequence['id']}/detections", agent_auth) assert len(dets) == 3 diff --git a/src/app/schemas/detections.py b/src/app/schemas/detections.py index a15a1d45..f30fa6c5 100644 --- a/src/app/schemas/detections.py +++ b/src/app/schemas/detections.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field from app.core.config import settings -from app.models import Detection, AnnotationType +from app.models import AnnotationType, Detection __all__ = ["Azimuth", "DetectionCreate", "DetectionLabel", "DetectionUrl"] diff --git a/src/app/schemas/sequences.py b/src/app/schemas/sequences.py index d51bc0cf..382aedbc 100644 --- a/src/app/schemas/sequences.py +++ b/src/app/schemas/sequences.py @@ -7,7 +7,7 @@ from pydantic import BaseModel -from app.models import Sequence, AnnotationType +from app.models import AnnotationType, Sequence __all__ = ["SequenceUpdate", "SequenceWithCone"] diff --git a/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py b/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py index 5e0aa0a6..d4f8b081 100644 --- a/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py +++ b/src/migrations/versions/2025_06_20_1945-42dzeg392dhu_fix_migration.py @@ -1,10 +1,9 @@ -from alembic import op import sqlalchemy as sa -import sqlmodel +from alembic import op # Ajoute ton identifiant de révision et dépendance si besoin -revision = '42dzeg392dhu' -down_revision = '4265426f8438' +revision = "42dzeg392dhu" +down_revision = "4265426f8438" branch_labels = None depends_on = None @@ -51,4 +50,4 @@ def downgrade() -> None: op.drop_column("detection", "sequence_id") op.drop_column("organization", "telegram_id") op.drop_column("camera", "last_image") - op.drop_table("sequences") \ No newline at end of file + op.drop_table("sequences") diff --git a/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py b/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py index 36bc01a4..0c5925a4 100644 --- a/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py +++ b/src/migrations/versions/2025_06_25_1720-2853acd1fc32_add_slack_hook.py @@ -22,4 +22,4 @@ def upgrade() -> None: def downgrade() -> None: - op.drop_column("organization", "slack_hook") \ No newline at end of file + op.drop_column("organization", "slack_hook") diff --git a/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py b/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py index 4ec19a8b..6fd81826 100644 --- a/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py +++ b/src/migrations/versions/2025_08_20_1647-307a1d6d490d_modify_is_wilfire_column.py @@ -5,27 +5,44 @@ Create Date: 2025-08-20 16:47:05.346210 """ + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa -import sqlmodel - +from alembic import op # revision identifiers, used by Alembic. -revision: str = '307a1d6d490d' -down_revision: Union[str, None] = '2853acd1fc32' +revision: str = "307a1d6d490d" +down_revision: Union[str, None] = "2853acd1fc32" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None # Define the new ENUM type annotation_type_enum = sa.Enum( - "WILDFIRE_SMOKE", "OTHER_SMOKE", "ANTENNA", "BUILDING", "CLIFF", "DARK", - "DUST", "HIGH_CLOUD", "LOW_CLOUD", "LENS_FLARE", "LENS_DROPLET", "LIGHT", - "RAIN", "TRAIL", "ROAD", "SKY", "TREE", "WATER_BODY", "DOUBT", "OTHER", - name="annotationtype" + "WILDFIRE_SMOKE", + "OTHER_SMOKE", + "ANTENNA", + "BUILDING", + "CLIFF", + "DARK", + "DUST", + "HIGH_CLOUD", + "LOW_CLOUD", + "LENS_FLARE", + "LENS_DROPLET", + "LIGHT", + "RAIN", + "TRAIL", + "ROAD", + "SKY", + "TREE", + "WATER_BODY", + "DOUBT", + "OTHER", + name="annotationtype", ) + def upgrade(): # Create the enum type in the database annotation_type_enum.create(op.get_bind(), checkfirst=True) @@ -41,6 +58,7 @@ def upgrade(): END """) + def downgrade(): # Revert the column back to a boolean (or previous enum if applicable) op.execute(""" @@ -54,4 +72,4 @@ def downgrade(): """) # Drop the enum type from the DB - annotation_type_enum.drop(op.get_bind(), checkfirst=True) \ No newline at end of file + annotation_type_enum.drop(op.get_bind(), checkfirst=True) From cbbf1a1e90518407ebb9058e280d1eaaabf56ebe Mon Sep 17 00:00:00 2001 From: ronan Date: Wed, 20 Aug 2025 23:09:09 +0200 Subject: [PATCH 5/9] fix client test --- client/pyroclient/client.py | 4 ++-- client/tests/test_client.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/pyroclient/client.py b/client/pyroclient/client.py index b36eb9ff..3a7af953 100644 --- a/client/pyroclient/client.py +++ b/client/pyroclient/client.py @@ -218,12 +218,12 @@ def fetch_detections(self) -> Response: timeout=self.timeout, ) - def label_sequence(self, sequence_id: int, is_wildfire: bool) -> Response: + def label_sequence(self, sequence_id: int, is_wildfire: str) -> Response: """Update the label of a sequence made by a camera >>> from pyroclient import client >>> api_client = Client("MY_USER_TOKEN") - >>> response = api_client.label_sequence(1, is_wildfire=True) + >>> response = api_client.label_sequence(1, is_wildfire="wildfire_smoke") Args: sequence_id: ID of the associated sequence entry diff --git a/client/tests/test_client.py b/client/tests/test_client.py index c82c8b45..bb9ff1c1 100644 --- a/client/tests/test_client.py +++ b/client/tests/test_client.py @@ -54,7 +54,7 @@ def test_agent_workflow(test_cam_workflow, agent_token): agent_client = Client(agent_token, "http://localhost:5050", timeout=10) response = agent_client.fetch_latest_sequences().json() assert len(response) == 1 - response = agent_client.label_sequence(response[0]["id"], True) + response = agent_client.label_sequence(response[0]["id"], "wildfire_smoke") assert response.status_code == 200, response.__dict__ From 2fbaea6771a27e08948c2e57d7a0cc0fb80f61d6 Mon Sep 17 00:00:00 2001 From: ronan Date: Thu, 28 Aug 2025 14:21:30 +0200 Subject: [PATCH 6/9] add two columns in cameras table --- src/app/models.py | 2 ++ src/app/schemas/cameras.py | 3 +- ...94_create_news_columns_in_cameras_table.py | 28 +++++++++++++++++++ src/tests/conftest.py | 4 +++ src/tests/endpoints/test_cameras.py | 8 ++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py diff --git a/src/app/models.py b/src/app/models.py index de7423a1..9f124240 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -70,6 +70,8 @@ class Camera(SQLModel, table=True): elevation: float = Field(..., gt=0, lt=10000, nullable=False) lat: float = Field(..., gt=-90, lt=90) lon: float = Field(..., gt=-180, lt=180) + ip_address: str + livestream_activated: bool = False is_trustable: bool = True last_active_at: Union[datetime, None] = None last_image: Union[str, None] = None diff --git a/src/app/schemas/cameras.py b/src/app/schemas/cameras.py index 006eb1ca..5d9380bf 100644 --- a/src/app/schemas/cameras.py +++ b/src/app/schemas/cameras.py @@ -32,7 +32,6 @@ class CameraEdit(BaseModel): lat: float = Field(..., gt=-90, lt=90, description="latitude", json_schema_extra={"examples": [44.765181]}) lon: float = Field(..., gt=-180, lt=180, description="longitude", json_schema_extra={"examples": [4.514880]}) - class CameraCreate(CameraEdit): organization_id: int = Field(..., gt=0) name: str = Field( @@ -49,6 +48,8 @@ class CameraCreate(CameraEdit): description="angle between left and right camera view", json_schema_extra={"examples": [120.0]}, ) + ip_address: str + livestream_activated: bool = False is_trustable: bool = Field(True, description="whether the detection from this camera can be trusted") diff --git a/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py b/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py new file mode 100644 index 00000000..90999c37 --- /dev/null +++ b/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py @@ -0,0 +1,28 @@ +"""create news columns in cameras table + +Revision ID: 47005ff54a94 +Revises: 307a1d6d490d +Create Date: 2025-08-28 11:05:46.058307 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '47005ff54a94' +down_revision: Union[str, None] = '307a1d6d490d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("camera", sa.Column("ip_address", sqlmodel.sql.sqltypes.AutoString(), nullable=False)) + op.add_column("camera", sa.Column("livestream_activated", sa.Boolean(), nullable=False)) + +def downgrade() -> None: + op.drop_column("camera", "ip_address") + op.drop_column("camera", "livestream_activated") \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ed02ddcc..197959ee 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -74,6 +74,8 @@ "elevation": 110.6, "lat": 3.6, "lon": -45.2, + "ip_address": "165.165.165.165", + "livestream_activated": False, "is_trustable": True, "last_active_at": datetime.strptime("2023-11-07T15:07:19.226673", dt_format), "last_image": None, @@ -87,6 +89,8 @@ "elevation": 110.6, "lat": 3.6, "lon": -45.2, + "ip_address": "165.165.165.165", + "livestream_activated": True, "is_trustable": False, "last_active_at": None, "last_image": None, diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index ab2061e9..ec7aca8f 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -17,6 +17,8 @@ "elevation": 30.0, "lat": 3.5, "lon": 7.8, + "ip_address": "165.165.165.165", + "livestream_activated": False, }, 401, "Not authenticated", @@ -36,6 +38,8 @@ "elevation": 30.0, "lat": 3.5, "lon": 7.8, + "ip_address": "165.165.165.165", + "livestream_activated": False, }, 201, None, @@ -49,6 +53,8 @@ "elevation": 30.0, "lat": 3.5, "lon": 7.8, + "ip_address": "165.165.165.165", + "livestream_activated": False, }, 201, None, @@ -62,6 +68,8 @@ "elevation": 30.0, "lat": 3.5, "lon": 7.8, + "ip_address": "165.165.165.165", + "livestream_activated": False, }, 403, "Incompatible token scope.", From 552955352dea23850f5acf5eee650ebd5ca8a07f Mon Sep 17 00:00:00 2001 From: ronan Date: Thu, 28 Aug 2025 15:03:05 +0200 Subject: [PATCH 7/9] fix style --- src/app/models.py | 2 +- src/app/schemas/cameras.py | 3 ++- ...005ff54a94_create_news_columns_in_cameras_table.py | 11 ++++++----- src/tests/conftest.py | 4 ++-- src/tests/endpoints/test_cameras.py | 8 ++++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/app/models.py b/src/app/models.py index 9f124240..397e5624 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -70,7 +70,7 @@ class Camera(SQLModel, table=True): elevation: float = Field(..., gt=0, lt=10000, nullable=False) lat: float = Field(..., gt=-90, lt=90) lon: float = Field(..., gt=-180, lt=180) - ip_address: str + ip_address: str livestream_activated: bool = False is_trustable: bool = True last_active_at: Union[datetime, None] = None diff --git a/src/app/schemas/cameras.py b/src/app/schemas/cameras.py index 5d9380bf..cf12b507 100644 --- a/src/app/schemas/cameras.py +++ b/src/app/schemas/cameras.py @@ -32,6 +32,7 @@ class CameraEdit(BaseModel): lat: float = Field(..., gt=-90, lt=90, description="latitude", json_schema_extra={"examples": [44.765181]}) lon: float = Field(..., gt=-180, lt=180, description="longitude", json_schema_extra={"examples": [4.514880]}) + class CameraCreate(CameraEdit): organization_id: int = Field(..., gt=0) name: str = Field( @@ -48,7 +49,7 @@ class CameraCreate(CameraEdit): description="angle between left and right camera view", json_schema_extra={"examples": [120.0]}, ) - ip_address: str + ip_address: str livestream_activated: bool = False is_trustable: bool = Field(True, description="whether the detection from this camera can be trusted") diff --git a/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py b/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py index 90999c37..62d7a4d9 100644 --- a/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py +++ b/src/migrations/versions/2025_08_28_1105-47005ff54a94_create_news_columns_in_cameras_table.py @@ -5,16 +5,16 @@ Create Date: 2025-08-28 11:05:46.058307 """ + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa import sqlmodel - +from alembic import op # revision identifiers, used by Alembic. -revision: str = '47005ff54a94' -down_revision: Union[str, None] = '307a1d6d490d' +revision: str = "47005ff54a94" +down_revision: Union[str, None] = "307a1d6d490d" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -23,6 +23,7 @@ def upgrade() -> None: op.add_column("camera", sa.Column("ip_address", sqlmodel.sql.sqltypes.AutoString(), nullable=False)) op.add_column("camera", sa.Column("livestream_activated", sa.Boolean(), nullable=False)) + def downgrade() -> None: op.drop_column("camera", "ip_address") - op.drop_column("camera", "livestream_activated") \ No newline at end of file + op.drop_column("camera", "livestream_activated") diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 197959ee..f8553d34 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -75,7 +75,7 @@ "lat": 3.6, "lon": -45.2, "ip_address": "165.165.165.165", - "livestream_activated": False, + "livestream_activated": False, "is_trustable": True, "last_active_at": datetime.strptime("2023-11-07T15:07:19.226673", dt_format), "last_image": None, @@ -90,7 +90,7 @@ "lat": 3.6, "lon": -45.2, "ip_address": "165.165.165.165", - "livestream_activated": True, + "livestream_activated": True, "is_trustable": False, "last_active_at": None, "last_image": None, diff --git a/src/tests/endpoints/test_cameras.py b/src/tests/endpoints/test_cameras.py index ec7aca8f..fd60b11f 100644 --- a/src/tests/endpoints/test_cameras.py +++ b/src/tests/endpoints/test_cameras.py @@ -18,7 +18,7 @@ "lat": 3.5, "lon": 7.8, "ip_address": "165.165.165.165", - "livestream_activated": False, + "livestream_activated": False, }, 401, "Not authenticated", @@ -39,7 +39,7 @@ "lat": 3.5, "lon": 7.8, "ip_address": "165.165.165.165", - "livestream_activated": False, + "livestream_activated": False, }, 201, None, @@ -54,7 +54,7 @@ "lat": 3.5, "lon": 7.8, "ip_address": "165.165.165.165", - "livestream_activated": False, + "livestream_activated": False, }, 201, None, @@ -69,7 +69,7 @@ "lat": 3.5, "lon": 7.8, "ip_address": "165.165.165.165", - "livestream_activated": False, + "livestream_activated": False, }, 403, "Incompatible token scope.", From a477ae65bbdc3bf290c8052aa94ef9f20cc62263 Mon Sep 17 00:00:00 2001 From: ronan Date: Thu, 28 Aug 2025 15:11:29 +0200 Subject: [PATCH 8/9] fix test client --- client/tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/tests/conftest.py b/client/tests/conftest.py index 5de6c59f..50524e1c 100644 --- a/client/tests/conftest.py +++ b/client/tests/conftest.py @@ -35,6 +35,8 @@ def cam_token(): "elevation": 1582, "lat": 44.765181, "lon": 4.51488, + "ip_address": "165.165.165.165", + "livestream_activated": False, "is_trustable": True, } response = requests.post(urljoin(API_URL, "cameras"), json=payload, headers=admin_headers, timeout=5) From 7bccb9bdb96d3ea0fb55371e959e3c26a7072d46 Mon Sep 17 00:00:00 2001 From: ronan Date: Thu, 28 Aug 2025 15:34:06 +0200 Subject: [PATCH 9/9] fix test e2e --- scripts/test_e2e.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test_e2e.py b/scripts/test_e2e.py index 74aea706..94e37a5b 100644 --- a/scripts/test_e2e.py +++ b/scripts/test_e2e.py @@ -78,6 +78,8 @@ def main(args): "lat": 44.7, "lon": 4.5, "azimuth": 110, + "ip_address": "165.165.165.165", + "livestream_activated": False, } cam_id = api_request("post", f"{args.endpoint}/cameras/", agent_auth, payload)["id"]