From ed8991a9f679e9959c85258b57458b30c224756b Mon Sep 17 00:00:00 2001 From: Mel Fagundes Date: Fri, 20 Jun 2025 17:27:38 -0300 Subject: [PATCH 1/2] Devtest Melissa Fagundes --- .env | 1 + Dockerfile | 9 ++++ README copy.md | 28 ++++++++++ app/__init__.py | 0 app/crud.py | 46 ++++++++++++++++ app/database.py | 9 ++++ app/models.py | 28 ++++++++++ app/routes.py | 31 +++++++++++ app/schemas.py | 25 +++++++++ export_events.py | 27 ++++++++++ main.py | 12 +++++ requirements.txt | 5 ++ .../test_main.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 8300 bytes tests/test_main.py | 49 ++++++++++++++++++ 14 files changed, 270 insertions(+) create mode 100644 .env create mode 100644 Dockerfile create mode 100644 README copy.md create mode 100644 app/__init__.py create mode 100644 app/crud.py create mode 100644 app/database.py create mode 100644 app/models.py create mode 100644 app/routes.py create mode 100644 app/schemas.py create mode 100644 export_events.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 tests/__pycache__/test_main.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/test_main.py diff --git a/.env b/.env new file mode 100644 index 0000000..b6da799 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:///./elevator.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e532d86 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY . /app + +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README copy.md b/README copy.md new file mode 100644 index 0000000..d6a587e --- /dev/null +++ b/README copy.md @@ -0,0 +1,28 @@ +# Elevator Resting Floor Data Collector + +## ๐Ÿš€ Objetivo +Registrar eventos de uso de elevador para alimentar um futuro modelo preditivo de "resting floor" ideal. + +## ๐Ÿ“ฆ Como executar + +### Ambiente local + +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +uvicorn main:app --reload +``` + +### Com Docker + +```bash +docker build -t elevator-api . +docker run -p 8000:8000 --env-file .env elevator-api +``` + +## ๐Ÿงช Testes + +```bash +pytest +``` diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 0000000..81228f9 --- /dev/null +++ b/app/crud.py @@ -0,0 +1,46 @@ +from sqlalchemy.orm import Session +from app import models +from datetime import datetime + +def create_elevator(db: Session, current_floor: int): + elevator = models.Elevator(current_floor=current_floor) + db.add(elevator) + db.commit() + db.refresh(elevator) + return elevator + +def update_elevator_status(db: Session, elevator_id: int, floor: int, is_moving: bool, is_occupied: bool): + elevator = db.query(models.Elevator).get(elevator_id) + if elevator: + from_floor = elevator.current_floor + elevator.current_floor = floor + elevator.is_moving = is_moving + elevator.is_occupied = is_occupied + elevator.last_updated = datetime.utcnow() + db.commit() + db.refresh(elevator) + + # Evento + db.add(models.ElevatorEvent( + elevator_id=elevator_id, + event_type="MOVE" if is_moving else "REST", + from_floor=from_floor, + to_floor=floor + )) + db.commit() + return elevator + return None + +def create_demand(db: Session, floor_called_from: int, elevator_id: int = 1): + demand = models.Demand(floor_called_from=floor_called_from) + db.add(demand) + db.add(models.ElevatorEvent( + elevator_id=elevator_id, + event_type="CALL", + from_floor=floor_called_from + )) + db.commit() + return demand + +def get_events(db: Session): + return db.query(models.ElevatorEvent).all() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..2c78e5d --- /dev/null +++ b/app/database.py @@ -0,0 +1,9 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +import os + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./elevator.db") + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) +Base = declarative_base() diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..50ce651 --- /dev/null +++ b/app/models.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime +from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime + +Base = declarative_base() + +class Elevator(Base): + __tablename__ = "elevators" + id = Column(Integer, primary_key=True, index=True) + current_floor = Column(Integer, nullable=False) + is_moving = Column(Boolean, default=False) + is_occupied = Column(Boolean, default=False) + last_updated = Column(DateTime, default=datetime.utcnow) + +class ElevatorEvent(Base): + __tablename__ = "events" + id = Column(Integer, primary_key=True, index=True) + elevator_id = Column(Integer, ForeignKey("elevators.id")) + event_type = Column(String, nullable=False) # CALL, MOVE, REST + from_floor = Column(Integer, nullable=True) + to_floor = Column(Integer, nullable=True) + timestamp = Column(DateTime, default=datetime.utcnow) + +class Demand(Base): + __tablename__ = "demands" + id = Column(Integer, primary_key=True, index=True) + floor_called_from = Column(Integer, nullable=False) + timestamp = Column(DateTime, default=datetime.utcnow) diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..612f799 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.database import SessionLocal +from app import crud, schemas + +router = APIRouter() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@router.post("/elevator/") +def create_elevator(payload: schemas.ElevatorCreate, db: Session = Depends(get_db)): + return crud.create_elevator(db, current_floor=payload.current_floor) + +@router.patch("/elevator/{elevator_id}/status") +def update_status(elevator_id: int, payload: schemas.ElevatorStatusUpdate, db: Session = Depends(get_db)): + result = crud.update_elevator_status(db, elevator_id, payload.current_floor, payload.is_moving, payload.is_occupied) + return {"status": "updated"} if result else {"status": "elevator not found"} + +@router.post("/demand/") +def create_demand(payload: schemas.DemandCreate, db: Session = Depends(get_db)): + crud.create_demand(db, floor_called_from=payload.floor_called_from) + return {"status": "demand recorded"} + +@router.get("/events/", response_model=list[schemas.Event]) +def get_all_events(db: Session = Depends(get_db)): + return crud.get_events(db) diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..4100ec4 --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class ElevatorCreate(BaseModel): + current_floor: int + +class ElevatorStatusUpdate(BaseModel): + current_floor: int + is_moving: bool + is_occupied: bool + +class DemandCreate(BaseModel): + floor_called_from: int + +class Event(BaseModel): + id: int + elevator_id: int + event_type: str + from_floor: Optional[int] + to_floor: Optional[int] + timestamp: datetime + + class Config: + orm_mode = True diff --git a/export_events.py b/export_events.py new file mode 100644 index 0000000..b58af03 --- /dev/null +++ b/export_events.py @@ -0,0 +1,27 @@ +import csv +from sqlalchemy.orm import Session +from app.models import ElevatorEvent +from app.database import SessionLocal + +def export_events_to_csv(filename="events_export.csv"): + db: Session = SessionLocal() + events = db.query(ElevatorEvent).all() + db.close() + + with open(filename, mode='w', newline='') as csv_file: + fieldnames = ['id', 'elevator_id', 'event_type', 'from_floor', 'to_floor', 'timestamp'] + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + + writer.writeheader() + for event in events: + writer.writerow({ + 'id': event.id, + 'elevator_id': event.elevator_id, + 'event_type': event.event_type, + 'from_floor': event.from_floor, + 'to_floor': event.to_floor, + 'timestamp': event.timestamp + }) + +if __name__ == "__main__": + export_events_to_csv() diff --git a/main.py b/main.py new file mode 100644 index 0000000..1f35c32 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI +from app.routes import router +from app.database import Base, engine + +Base.metadata.create_all(bind=engine) + +app = FastAPI() +app.include_router(router) + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71c6792 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +sqlalchemy +pydantic +pytest diff --git a/tests/__pycache__/test_main.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_main.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fec0835b7a63fa2363ff5f7cf2ea063c8c986391 GIT binary patch literal 8300 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 literal 0 HcmV?d00001 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..21470b9 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,49 @@ +import pytest +from fastapi.testclient import TestClient +from main import app, Base, engine, SessionLocal + +client = TestClient(app) + +@pytest.fixture(autouse=True) +def setup_and_teardown(): + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) + yield + Base.metadata.drop_all(bind=engine) + +def test_create_elevator(): + response = client.post("/elevator/", json={"current_floor": 0}) + assert response.status_code == 200 + assert "id" in response.json() + +def test_update_elevator_status(): + elevator = client.post("/elevator/", json={"current_floor": 0}).json() + elevator_id = elevator["id"] + payload = { + "current_floor": 3, + "is_moving": True, + "is_occupied": False + } + response = client.patch(f"/elevator/{elevator_id}/status", json=payload) + assert response.status_code == 200 + assert response.json()["status"] == "updated" + +def test_create_demand(): + client.post("/elevator/", json={"current_floor": 0}) # Cria elevador 1 + response = client.post("/demand/", json={"floor_called_from": 5}) + assert response.status_code == 200 + assert response.json()["status"] == "demand recorded" + +def test_events_logged(): + client.post("/elevator/", json={"current_floor": 0}) + client.post("/demand/", json={"floor_called_from": 2}) + client.patch("/elevator/1/status", json={ + "current_floor": 5, + "is_moving": True, + "is_occupied": False + }) + response = client.get("/events/") + assert response.status_code == 200 + events = response.json() + assert any(e["event_type"] == "CALL" for e in events) + assert any(e["event_type"] == "MOVE" for e in events) From c143ce0d250fc9373b8756edef71798bc51191b5 Mon Sep 17 00:00:00 2001 From: Mel Fagundes Date: Fri, 20 Jun 2025 18:06:27 -0300 Subject: [PATCH 2/2] Update README copy.md --- README copy.md | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/README copy.md b/README copy.md index d6a587e..22da303 100644 --- a/README copy.md +++ b/README copy.md @@ -1,28 +1,14 @@ # Elevator Resting Floor Data Collector -## ๐Ÿš€ Objetivo -Registrar eventos de uso de elevador para alimentar um futuro modelo preditivo de "resting floor" ideal. +## ๐Ÿš€ Objective +Record elevator usage events to later train a predictive model that suggests the ideal "resting floor" for an elevator. -## ๐Ÿ“ฆ Como executar +## ๐Ÿ“ฆ How to Run -### Ambiente local +### Local Environment ```bash python -m venv venv source venv/bin/activate pip install -r requirements.txt uvicorn main:app --reload -``` - -### Com Docker - -```bash -docker build -t elevator-api . -docker run -p 8000:8000 --env-file .env elevator-api -``` - -## ๐Ÿงช Testes - -```bash -pytest -```