Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added app/__init__.py
Empty file.
Binary file added app/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added app/__pycache__/main.cpython-311.pyc
Binary file not shown.
Empty file added app/database/__init__.py
Empty file.
Binary file added app/database/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added app/database/__pycache__/elevators.cpython-311.pyc
Binary file not shown.
14 changes: 14 additions & 0 deletions app/database/elevators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase

DATABASE_URL = "sqlite:///./elevators.db"

engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
)

SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)

class Base(DeclarativeBase):
pass
26 changes: 26 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging
from fastapi import FastAPI
from dotenv import load_dotenv

from app.database.elevators import Base, engine
from app.routers.elevators import router as main_router

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="Prediction_Engine")


@app.on_event("startup")
def on_startup():
"""
Function executed once when the FastAPI app starts.
It ensures that all database tables defined in the SQLAlchemy models are created.
"""
Base.metadata.create_all(bind=engine)
logger.info("SQLite schema ready")


app.include_router(main_router)
Empty file added app/models/__init__.py
Empty file.
Binary file added app/models/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added app/models/__pycache__/elevators.cpython-311.pyc
Binary file not shown.
18 changes: 18 additions & 0 deletions app/models/elevators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from datetime import datetime
from sqlalchemy import Column, Integer, DateTime, Boolean
from app.database.elevators import Base

class Demand(Base):
__tablename__ = "demands"

id = Column(Integer, primary_key=True, index=True)
floor = Column(Integer, nullable=False)
timestamp = Column(DateTime, default=datetime.utcnow) # ← OK aqui


class ElevatorState(Base):
__tablename__ = "elevator_states"
id = Column(Integer, primary_key=True, index=True)
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
floor = Column(Integer, nullable=False)
vacant = Column(Boolean, nullable=False)
Empty file added app/routers/__init__.py
Empty file.
Binary file added app/routers/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added app/routers/__pycache__/elevators.cpython-311.pyc
Binary file not shown.
88 changes: 88 additions & 0 deletions app/routers/elevators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.models.elevators import Demand, ElevatorState
from app.schemas.elevators import DemandOut, DemandCreate, StateOut, StateCreate
from app.database.elevators import SessionLocal

router = APIRouter(prefix="/api", tags=["elevators"])


def get_db():
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()


# POST /demands
@router.post("/demands", response_model=DemandOut)
def create_demand(demand: DemandCreate, db: Session = Depends(get_db)):
"""
Registers a new elevator demand when a user calls the elevator from a specific floor.
"""
rec = Demand(floor=demand.floor)
db.add(rec)
db.flush()
db.refresh(rec)
return rec


# POST /state
@router.post("/state", response_model=StateOut)
def create_state(state: StateCreate, db: Session = Depends(get_db)):
"""
Records the elevator’s current state, including its current floor and whether it is vacant.
"""
rec = ElevatorState(floor=state.floor, vacant=state.vacant)
db.add(rec)
db.flush()
db.refresh(rec)
return rec


# GET /analytics
@router.get("/analytics")
def analyze_positioning(db: Session = Depends(get_db), limit: int = 50, threshold: int = 3):
"""
Analyzes elevator positioning efficiency by comparing resting positions with upcoming demands.
"""
states = (
db.query(ElevatorState)
.order_by(ElevatorState.timestamp.asc())
.limit(limit)
.all()
)

if not states:
raise HTTPException(status_code=404, detail="No elevator states found.")

distances = []

for state in states:
demand = (
db.query(Demand)
.filter(Demand.timestamp > state.timestamp)
.order_by(Demand.timestamp.asc())
.first()
)
if demand:
distance = abs(demand.floor - state.floor)
distances.append(distance)

if not distances:
raise HTTPException(status_code=404, detail="Not enough state → demand pairs found.")

avg_distance = sum(distances) / len(distances)

return {
"average_distance": round(avg_distance, 2),
"threshold": threshold,
"mispositioned": avg_distance > threshold,
"message": "Elevator is frequently poorly positioned." if avg_distance > threshold else "Positioning is within acceptable range."
}
Empty file added app/schemas/__init__.py
Empty file.
Binary file added app/schemas/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added app/schemas/__pycache__/elevators.cpython-311.pyc
Binary file not shown.
27 changes: 27 additions & 0 deletions app/schemas/elevators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from datetime import datetime
from pydantic import BaseModel, Field


class DemandCreate(BaseModel):
floor: int = Field(..., ge=0, description="Andar onde o usuário chamou o elevador")

class DemandOut(DemandCreate):
id: int
timestamp: datetime

model_config = {
"from_attributes": True # substitui orm_mode no Pydantic v2
}


class StateCreate(BaseModel):
floor: int
vacant: bool

class StateOut(StateCreate):
id: int
timestamp: datetime

model_config = {
"from_attributes": True
}
10 changes: 0 additions & 10 deletions chatgpt/app_tests.py

This file was deleted.

12 changes: 0 additions & 12 deletions chatgpt/db.sql

This file was deleted.

43 changes: 0 additions & 43 deletions chatgpt/main.py

This file was deleted.

4 changes: 0 additions & 4 deletions chatgpt/requirements.txt

This file was deleted.

Binary file added elevators.db
Binary file not shown.
53 changes: 53 additions & 0 deletions explanation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
### Problem Understanding
The goal was to build a minimal backend system to record elevator demands and resting states, so that historical data can be used later by an ML model to predict the optimal resting floor.

### Design Decisions
1. Database Modeling
I created two tables:

Demand: stores the floor where the elevator was called and the timestamp.

ElevatorState: stores the elevator’s floor, timestamp, and whether it's vacant.

This separation allows:

Compare demand vs. elevator location.

Simulate a real scenario with frequent calls and elevator resting positions.

Alternative: I considered using a single table, but separating concerns made analysis easier and aligned with normalized schema practices.

2. API Endpoints
POST /api/demands: register a new demand.

POST /api/state: register the elevator’s current floor and availability.

GET /api/analytics: analyze the average distance between each recorded elevator state and the subsequent demand. This helps evaluate whether the elevator is frequently poorly positioned when idle.

This gives ML practitioners useful data such as:

Time series of elevator calls.

Reactions or delays from elevator states.

Or initiate a model retraining process to improve floor prediction accuracy based on updated behavior patterns.

3. Business Rules Added
In GET /analytics, we check if the elevator is often resting too far from the next demand, using a configurable threshold.

This acts as an early signal of poor optimization.

4. Testing
Covered endpoints with pytest and TestClient.

Verified status codes and response content.

Ensures system stability before plugging in any ML logic.

### Tech Stack
FastAPI + SQLite.

SQLAlchemy for ORM.

Pytest for testing.

21 changes: 21 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Dev Test

## RUN APP
Instruction to run application

### Create the virtual environment
python -m venv test_dev

### Activate the virtual environment
source test_dev/bin/activate (Linux)

### Install packages
pip install -r requirements.txt

### Start the API server
uvicorn app.main:app --reload

### Run tests
pytest tests/test_endpoints.py

### Explanation
The file explanation.md has the explanation about the solution.

## Elevators
When an elevator is empty and not moving this is known as it's resting floor.
The ideal resting floor to be positioned on depends on the likely next floor that the elevator will be called from.
Expand Down
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fastapi==0.115.14
uvicorn==0.35.0
SQLAlchemy==2.0.41
pydantic==2.11.7
python-dotenv==1.1.1
pytest==8.4.1
httpx==0.28.1
Empty file added tests/__init__.py
Empty file.
Binary file added tests/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
40 changes: 40 additions & 0 deletions tests/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)


def test_create_demand():
resp = client.post("/api/demands", json={"floor": 3})
assert resp.status_code == 200
payload = resp.json()
assert payload["floor"] == 3
assert "id" in payload
assert "timestamp" in payload


def test_create_state():
resp = client.post("/api/state", json={"floor": 2, "vacant": True})
assert resp.status_code == 200
payload = resp.json()
assert payload["floor"] == 2
assert payload["vacant"] is True
assert "timestamp" in payload


def test_analyze_positioning():
# Criar um state antigo
client.post("/api/state", json={"floor": 1, "vacant": True})

# Criar uma demanda posterior
client.post("/api/demands", json={"floor": 5})

# Chamar a rota de análise
resp = client.get("/api/analytics", params={"limit": 10, "threshold": 2})
assert resp.status_code == 200
data = resp.json()

assert "average_distance" in data
assert "mispositioned" in data
assert isinstance(data["average_distance"], float)
assert isinstance(data["mispositioned"], bool)