From e45faeb281b6502ac18d211d8ef4ac5cb0242d6f Mon Sep 17 00:00:00 2001 From: rhinemann Date: Sun, 22 Feb 2026 11:22:19 +0100 Subject: [PATCH 1/7] updated compose file --- agent/docker/docker-compose.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/agent/docker/docker-compose.yaml b/agent/docker/docker-compose.yaml index 3c8b1c4..3059c9e 100644 --- a/agent/docker/docker-compose.yaml +++ b/agent/docker/docker-compose.yaml @@ -1,4 +1,3 @@ -version: "3.9" name: "road_vision" services: mqtt: From a63864bcaaea7a1e325de65f6b8f2093ab9142a9 Mon Sep 17 00:00:00 2001 From: rhinemann Date: Sun, 22 Feb 2026 12:17:03 +0100 Subject: [PATCH 2/7] updated compose file --- store/docker/docker-compose.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/store/docker/docker-compose.yaml b/store/docker/docker-compose.yaml index 8f9c32a..dff34e5 100644 --- a/store/docker/docker-compose.yaml +++ b/store/docker/docker-compose.yaml @@ -1,8 +1,7 @@ -version: "3.9" name: "road_vision__database" services: postgres_db: - image: postgres:latest + image: postgres:17 container_name: postgres_db restart: always environment: From 1e7516fe7bb88b46a1c8feca1636c51a5e4805d3 Mon Sep 17 00:00:00 2001 From: rhinemann Date: Sun, 22 Feb 2026 12:19:33 +0100 Subject: [PATCH 3/7] update requirements --- store/requirements.txt | Bin 758 -> 450 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/store/requirements.txt b/store/requirements.txt index f920843a968a3ddc817af19433867a2ab1f2a0af..b70b2974d512b1220c5c1f801d0529d2d046a579 100644 GIT binary patch literal 450 zcmZ9Ju@1s83`B26;!_yl57PtO$Hz_-^@ zYps&s5owsE;yX^h)0Wth4hvXH<{fgT?e3muGfq<*ci6yE>j3u3YF*-UdCCXs*1c`e ZrMe!jm=sLe_BH;69_xhdiM7}<<6lW)PKf{j literal 758 zcmZva!A`?K3`BiK;!{9Ps0ti7^bQh;6Q`=AX&bd{q9#!C^S~Q#mQ)C>tVBCA_SoL< zucZY}ZJjNN(pGlCx3`A>*6uhHyR;cGv2#0v6+kN#!mhZ#CNjqg+k+IgbL__Ng-chU z*b@gbyb&q$6xPfcYi?Wj(U2wK8Ff`9cg_ZV$qD2t;U*9A82xSWlAWlPKOr58hN#`k zf8AB|9|m`-I`ygzzIyMVu?65$=o4?ZNIHjk`*jT2z&-IcaB7tG76nXL~|7XPyQ1N_Nx# zI#P{%@E1~9QtmD>? jOty1>>Oz|zcbBv6l<5fa3Rc72@!t(Ry$EsCx3={IFM(~< From 69e679eccff3c30575ed358ab34dd3af98ba00b8 Mon Sep 17 00:00:00 2001 From: AndriiJushchenko Date: Tue, 24 Feb 2026 22:18:36 +0200 Subject: [PATCH 4/7] =?UTF-8?q?SCRUM-[49,=2054]=20=D0=A0=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D1=96=D0=B7=D1=83=D0=B2=D0=B0=D1=82=D0=B8=20POST=20=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B2=D1=96=D0=B4=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D0=BE=20websoket=20=D0=BF=D1=96=D0=B4=20=D1=87=D0=B0?= =?UTF-8?q?=D1=81=20=D0=B2=D0=B8=D0=BA=D0=BE=D0=BD=D0=B0=D0=BD=D0=BD=D1=8F?= =?UTF-8?q?=20POST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- store/main.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/store/main.py b/store/main.py index 272a646..3edd1af 100644 --- a/store/main.py +++ b/store/main.py @@ -2,6 +2,7 @@ import asyncio import json from typing import Set, Dict, List, Any from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Body +from fastapi.encoders import jsonable_encoder from sqlalchemy import ( create_engine, MetaData, @@ -126,9 +127,34 @@ async def send_data_to_subscribers(user_id: int, data): @app.post("/processed_agent_data/") async def create_processed_agent_data(data: List[ProcessedAgentData]): - # Insert data to database - # Send data to subscribers - pass + session = SessionLocal() + try: + created_data = [ + { + "road_state": item.road_state, + "x": item.agent_data.accelerometer.x, + "y": item.agent_data.accelerometer.y, + "z": item.agent_data.accelerometer.z, + "latitude": item.agent_data.gps.latitude, + "longitude": item.agent_data.gps.longitude, + "timestamp": item.agent_data.timestamp, + } + for item in data + ] + stmt = processed_agent_data.insert().values(created_data).returning(processed_agent_data) + result = session.execute(stmt) + created_records = [dict(row._mapping) for row in result.fetchall()] + session.commit() + + for record in created_records: + await send_data_to_subscribers(jsonable_encoder(record)) + return created_records + except Exception as err: + session.rollback() + print(f"Database error: {err}") + raise HTTPException(status_code=500, detail="Internal Server Error") + finally: + session.close() @app.get( From f3512e4afb26739c92cdc7bcbb3274c9ac688a55 Mon Sep 17 00:00:00 2001 From: AndriiJushchenko Date: Wed, 25 Feb 2026 19:05:25 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=D0=A2=D1=80=D0=BE=D1=85=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=84=D1=96=D0=BA=D1=81=D0=B8=D0=B2=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D1=96=D1=8E=20post=20=D1=96=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D1=96=D0=B2=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=20post=20?= =?UTF-8?q?=D1=96=20websoket.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- store/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/store/main.py b/store/main.py index 3edd1af..81d51a1 100644 --- a/store/main.py +++ b/store/main.py @@ -126,12 +126,13 @@ async def send_data_to_subscribers(user_id: int, data): @app.post("/processed_agent_data/") -async def create_processed_agent_data(data: List[ProcessedAgentData]): +async def create_processed_agent_data(data: List[ProcessedAgentData], user_id: int = Body(..., embed=True)): session = SessionLocal() try: created_data = [ { "road_state": item.road_state, + "user_id": user_id, "x": item.agent_data.accelerometer.x, "y": item.agent_data.accelerometer.y, "z": item.agent_data.accelerometer.z, @@ -147,7 +148,7 @@ async def create_processed_agent_data(data: List[ProcessedAgentData]): session.commit() for record in created_records: - await send_data_to_subscribers(jsonable_encoder(record)) + await send_data_to_subscribers(user_id, jsonable_encoder(record)) return created_records except Exception as err: session.rollback() From 98fb6aa12a4961c440ac40c3f8903822421a7466 Mon Sep 17 00:00:00 2001 From: Senya Date: Thu, 26 Feb 2026 11:20:47 +0200 Subject: [PATCH 6/7] SCRUM-48: feature with CRUD (GET) --- .idea/.gitignore | 3 + .idea/IoT-Systems.iml | 15 + .idea/dbnavigator.xml | 425 ++++++++++++++++++ .idea/inspectionProfiles/Project_Default.xml | 16 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + store/.gitignore | 4 +- store/__init__.py | 0 store/database.py | 15 + store/main.py | 103 ++--- store/schemas.py | 51 +++ 13 files changed, 584 insertions(+), 75 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/IoT-Systems.iml create mode 100644 .idea/dbnavigator.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 store/__init__.py create mode 100644 store/database.py create mode 100644 store/schemas.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/IoT-Systems.iml b/.idea/IoT-Systems.iml new file mode 100644 index 0000000..688aa87 --- /dev/null +++ b/.idea/IoT-Systems.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..3b61c61 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..977601b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..df29e35 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5aef1d1 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/store/.gitignore b/store/.gitignore index 75b0912..f810ad9 100644 --- a/store/.gitignore +++ b/store/.gitignore @@ -1,3 +1,5 @@ venv __pycache__ -.idea \ No newline at end of file +.idea + +.idea/ diff --git a/store/__init__.py b/store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/store/database.py b/store/database.py new file mode 100644 index 0000000..7d86055 --- /dev/null +++ b/store/database.py @@ -0,0 +1,15 @@ +from sqlalchemy import MetaData +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +from config import POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB + + +DATABASE_URL = f"postgresql+psycopg2://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" +engine = create_engine(DATABASE_URL) + +Base = declarative_base() + +metadata = MetaData() + +SessionLocal = sessionmaker(bind=engine) diff --git a/store/main.py b/store/main.py index 81d51a1..fb61018 100644 --- a/store/main.py +++ b/store/main.py @@ -1,11 +1,8 @@ -import asyncio import json -from typing import Set, Dict, List, Any +from typing import Set, Dict, List from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Body from fastapi.encoders import jsonable_encoder from sqlalchemy import ( - create_engine, - MetaData, Table, Column, Integer, @@ -13,25 +10,14 @@ from sqlalchemy import ( Float, DateTime, ) -from sqlalchemy.orm import sessionmaker from sqlalchemy.sql import select -from datetime import datetime -from pydantic import BaseModel, field_validator -from config import ( - POSTGRES_HOST, - POSTGRES_PORT, - POSTGRES_DB, - POSTGRES_USER, - POSTGRES_PASSWORD, -) + +from database import metadata, SessionLocal +from schemas import ProcessedAgentData, ProcessedAgentDataInDB # FastAPI app setup app = FastAPI() -# SQLAlchemy setup -DATABASE_URL = f"postgresql+psycopg2://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" -engine = create_engine(DATABASE_URL) -metadata = MetaData() -# Define the ProcessedAgentData table + processed_agent_data = Table( "processed_agent_data", metadata, @@ -45,57 +31,6 @@ processed_agent_data = Table( Column("longitude", Float), Column("timestamp", DateTime), ) -SessionLocal = sessionmaker(bind=engine) - - -# SQLAlchemy model -class ProcessedAgentDataInDB(BaseModel): - id: int - road_state: str - user_id: int - x: float - y: float - z: float - latitude: float - longitude: float - timestamp: datetime - - -# FastAPI models -class AccelerometerData(BaseModel): - x: float - y: float - z: float - - -class GpsData(BaseModel): - latitude: float - longitude: float - - -class AgentData(BaseModel): - user_id: int - accelerometer: AccelerometerData - gps: GpsData - timestamp: datetime - - @classmethod - @field_validator("timestamp", mode="before") - def check_timestamp(cls, value): - if isinstance(value, datetime): - return value - try: - return datetime.fromisoformat(value) - except (TypeError, ValueError): - raise ValueError( - "Invalid timestamp format. Expected ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)." - ) - - -class ProcessedAgentData(BaseModel): - road_state: str - agent_data: AgentData - # WebSocket subscriptions subscriptions: Dict[int, Set[WebSocket]] = {} @@ -163,14 +98,34 @@ async def create_processed_agent_data(data: List[ProcessedAgentData], user_id: i response_model=ProcessedAgentDataInDB, ) def read_processed_agent_data(processed_agent_data_id: int): - # Get data by id - pass + session = SessionLocal() + try: + stmt = select(processed_agent_data).where( + processed_agent_data.c.id == processed_agent_data_id + ) + res = session.execute(stmt).fetchone() + if not res: + raise HTTPException(status_code=404, detail="Not found") + + return dict(res._mapping) + + finally: + session.close() @app.get("/processed_agent_data/", response_model=list[ProcessedAgentDataInDB]) def list_processed_agent_data(): - # Get list of data - pass + session = SessionLocal() + try: + stmt = select(processed_agent_data) + res = session.execute(stmt).fetchall() + if not res: + raise HTTPException(status_code=404, detail="Not found") + + return [dict(r._mapping) for r in res] + + finally: + session.close() @app.put( diff --git a/store/schemas.py b/store/schemas.py new file mode 100644 index 0000000..3d13dee --- /dev/null +++ b/store/schemas.py @@ -0,0 +1,51 @@ +from datetime import datetime + +from pydantic import BaseModel, field_validator + + +class ProcessedAgentDataInDB(BaseModel): + id: int + road_state: str + user_id: int + x: float + y: float + z: float + latitude: float + longitude: float + timestamp: datetime + + +# FastAPI models +class AccelerometerData(BaseModel): + x: float + y: float + z: float + + +class GpsData(BaseModel): + latitude: float + longitude: float + + +class AgentData(BaseModel): + user_id: int + accelerometer: AccelerometerData + gps: GpsData + timestamp: datetime + + @classmethod + @field_validator("timestamp", mode="before") + def check_timestamp(cls, value): + if isinstance(value, datetime): + return value + try: + return datetime.fromisoformat(value) + except (TypeError, ValueError): + raise ValueError( + "Invalid timestamp format. Expected ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)." + ) + + +class ProcessedAgentData(BaseModel): + road_state: str + agent_data: AgentData From 185b0aae58dbd450adb1ca52724a8aa442ecae5c Mon Sep 17 00:00:00 2001 From: anastasia-sl Date: Thu, 26 Feb 2026 14:55:51 +0200 Subject: [PATCH 7/7] implemented update and delete endpoints --- store/main.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/store/main.py b/store/main.py index 272a646..4b20dfe 100644 --- a/store/main.py +++ b/store/main.py @@ -152,7 +152,41 @@ def list_processed_agent_data(): ) def update_processed_agent_data(processed_agent_data_id: int, data: ProcessedAgentData): # Update data - pass + session = SessionLocal() + + try: + query = select(processed_agent_data).where( + processed_agent_data.c.id == processed_agent_data_id + ) + result = session.execute(query).fetchone() + + if not result: + raise HTTPException(status_code=404, detail="Data not found") + + update_query = ( + processed_agent_data.update() + .where(processed_agent_data.c.id == processed_agent_data_id) + .values( + road_state=data.road_state, + user_id=data.agent_data.user_id, + x=data.agent_data.accelerometer.x, + y=data.agent_data.accelerometer.y, + z=data.agent_data.accelerometer.z, + latitude=data.agent_data.gps.latitude, + longitude=data.agent_data.gps.longitude, + timestamp=data.agent_data.timestamp, + ) + ) + + session.execute(update_query) + session.commit() + + updated_result = session.execute(query).fetchone() + + return ProcessedAgentDataInDB(**updated_result._mapping) + + finally: + session.close() @app.delete( @@ -161,8 +195,28 @@ def update_processed_agent_data(processed_agent_data_id: int, data: ProcessedAge ) def delete_processed_agent_data(processed_agent_data_id: int): # Delete by id - pass + session = SessionLocal() + try: + query = select(processed_agent_data).where( + processed_agent_data.c.id == processed_agent_data_id + ) + result = session.execute(query).fetchone() + + if not result: + raise HTTPException(status_code=404, detail="Data not found") + + delete_query = processed_agent_data.delete().where( + processed_agent_data.c.id == processed_agent_data_id + ) + + session.execute(delete_query) + session.commit() + + return ProcessedAgentDataInDB(**result._mapping) + + finally: + session.close() if __name__ == "__main__": import uvicorn