feat: Refactor API structure and implement core functionalities

- Updated requirements to include asyncpg, pyyaml, aiofiles, and httpx.
- Modified schema.sql to add 'tipo_pasto' column in 'pasti' table.
- Created .gitignore file to exclude unnecessary files and directories.
- Added README.md for API implementation details and directory structure.
- Introduced config.yaml for centralized configuration management.
- Implemented core modules for database management, authentication, and exception handling.
- Developed models for 'pietanze', 'pasti', and 'prenotazioni' with validation.
- Created routes for CRUD operations on 'pietanze' with pagination and filtering.
- Established logging and error handling mechanisms throughout the application.
- Set up FastAPI application with CORS and health check endpoint.
This commit is contained in:
Matteo Benedetto
2025-06-05 18:31:36 +02:00
parent a394ed9ad2
commit d592e0f32b
20 changed files with 1110 additions and 96 deletions
+27
View File
@@ -0,0 +1,27 @@
from .pietanze import PietanzaBase, PietanzaCreate, PietanzaUpdate, PietanzaResponse
from .pasti import PastoBase, PastoCreate, PastoUpdate, PastoResponse, TipoPasto
from .prenotazioni import PrenotazioneBase, PrenotazioneCreate, PrenotazioneUpdate, PrenotazioneResponse, StatoPrenotazione
from .common import ErrorResponse, PaginatedResponse
__all__ = [
# Pietanze models
"PietanzaBase",
"PietanzaCreate",
"PietanzaUpdate",
"PietanzaResponse",
# Pasti models
"PastoBase",
"PastoCreate",
"PastoUpdate",
"PastoResponse",
"TipoPasto",
# Prenotazioni models
"PrenotazioneBase",
"PrenotazioneCreate",
"PrenotazioneUpdate",
"PrenotazioneResponse",
"StatoPrenotazione",
# Common models
"ErrorResponse",
"PaginatedResponse"
]
+14
View File
@@ -0,0 +1,14 @@
from pydantic import BaseModel
from typing import List, Optional, Any
class ErrorResponse(BaseModel):
error: str
detail: Optional[str] = None
code: Optional[str] = None
class PaginatedResponse(BaseModel):
items: List[Any]
total: int
page: int
size: int
pages: int
+101
View File
@@ -0,0 +1,101 @@
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Dict, Any
from datetime import datetime, date
from enum import Enum
class TipoPasto(str, Enum):
pranzo = "pranzo"
cena = "cena"
class PastoBase(BaseModel):
data_pasto: date
tipo_pasto: TipoPasto = TipoPasto.pranzo
portate: Optional[Dict[str, Dict[int, int]]] = Field(
default_factory=dict,
description="Portate structure: {'primo': {pietanza_id: max_prenotazioni}}"
)
turni: Optional[Dict[str, int]] = Field(
default_factory=dict,
description="Turni structure: {'12:45': max_posti}"
)
disponibile: bool = True
@validator('portate')
def validate_portate(cls, v):
if v is None:
return {}
# Validate structure: each portata should have pietanza_id -> max_count mapping
valid_portate = ['primo', 'secondo', 'contorno', 'dolce', 'frutta']
for portata, pietanze in v.items():
if portata not in valid_portate:
raise ValueError(f"Portata non valida: {portata}")
if not isinstance(pietanze, dict):
raise ValueError(f"Pietanze per {portata} deve essere un dizionario")
for pietanza_id, max_count in pietanze.items():
if not isinstance(int(pietanza_id), int) or int(pietanza_id) <= 0:
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
if not isinstance(max_count, int) or max_count < 0:
raise ValueError(f"Numero massimo prenotazioni non valido per pietanza {pietanza_id}")
return v
@validator('turni')
def validate_turni(cls, v):
if v is None:
return {}
# Validate time format and capacity
import re
time_pattern = re.compile(r'^([01]\d|2[0-3]):([0-5]\d)$')
for turno, capacity in v.items():
if not time_pattern.match(turno):
raise ValueError(f"Formato orario non valido: {turno}. Usare HH:MM")
if not isinstance(capacity, int) or capacity < 0:
raise ValueError(f"Capacità non valida per turno {turno}: {capacity}")
return v
class PastoCreate(PastoBase):
pass
class PastoUpdate(BaseModel):
tipo_pasto: Optional[TipoPasto] = None
portate: Optional[Dict[str, Dict[int, int]]] = None
turni: Optional[Dict[str, int]] = None
disponibile: Optional[bool] = None
# Same validators as PastoBase for optional fields
@validator('portate')
def validate_portate(cls, v):
if v is None:
return v
valid_portate = ['primo', 'secondo', 'contorno', 'dolce', 'frutta']
for portata, pietanze in v.items():
if portata not in valid_portate:
raise ValueError(f"Portata non valida: {portata}")
if not isinstance(pietanze, dict):
raise ValueError(f"Pietanze per {portata} deve essere un dizionario")
for pietanza_id, max_count in pietanze.items():
if not isinstance(int(pietanza_id), int) or int(pietanza_id) <= 0:
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
if not isinstance(max_count, int) or max_count < 0:
raise ValueError(f"Numero massimo prenotazioni non valido per pietanza {pietanza_id}")
return v
@validator('turni')
def validate_turni(cls, v):
if v is None:
return v
import re
time_pattern = re.compile(r'^([01]\d|2[0-3]):([0-5]\d)$')
for turno, capacity in v.items():
if not time_pattern.match(turno):
raise ValueError(f"Formato orario non valido: {turno}. Usare HH:MM")
if not isinstance(capacity, int) or capacity < 0:
raise ValueError(f"Capacità non valida per turno {turno}: {capacity}")
return v
class PastoResponse(PastoBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
+49
View File
@@ -0,0 +1,49 @@
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import datetime
class PietanzaBase(BaseModel):
nome: str = Field(..., min_length=1, max_length=150)
descrizione: Optional[str] = None
allergeni: Optional[List[str]] = []
class PietanzaCreate(PietanzaBase):
@validator('allergeni')
def validate_allergeni(cls, v):
if v is None:
return []
# Common allergens validation
valid_allergeni = {
'glutine', 'lattosio', 'uova', 'pesce', 'crostacei', 'arachidi',
'frutta_a_guscio', 'soia', 'sedano', 'senape', 'sesamo', 'solfiti'
}
for allergen in v:
if allergen not in valid_allergeni:
raise ValueError(f"Allergene non valido: {allergen}")
return v
class PietanzaUpdate(BaseModel):
nome: Optional[str] = Field(None, min_length=1, max_length=150)
descrizione: Optional[str] = None
allergeni: Optional[List[str]] = None
@validator('allergeni')
def validate_allergeni(cls, v):
if v is None:
return v
valid_allergeni = {
'glutine', 'lattosio', 'uova', 'pesce', 'crostacei', 'arachidi',
'frutta_a_guscio', 'soia', 'sedano', 'senape', 'sesamo', 'solfiti'
}
for allergen in v:
if allergen not in valid_allergeni:
raise ValueError(f"Allergene non valido: {allergen}")
return v
class PietanzaResponse(PietanzaBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
+63
View File
@@ -0,0 +1,63 @@
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import datetime
from enum import Enum
class StatoPrenotazione(str, Enum):
attiva = "attiva"
servita = "servita"
pagata = "pagata"
annullata = "annullata"
completata = "completata"
class PrenotazioneBase(BaseModel):
pasto_id: int
pietanze_selezionate: List[int] = Field(
description="Lista ID delle pietanze selezionate"
)
note: Optional[str] = Field(None, max_length=500)
stato: StatoPrenotazione = StatoPrenotazione.attiva
@validator('pietanze_selezionate')
def validate_pietanze_selezionate(cls, v):
if not v or len(v) == 0:
raise ValueError("Almeno una pietanza deve essere selezionata")
# Check for duplicates
if len(v) != len(set(v)):
raise ValueError("Non sono ammesse pietanze duplicate")
# Validate each ID
for pietanza_id in v:
if not isinstance(pietanza_id, int) or pietanza_id <= 0:
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
return v
class PrenotazioneCreate(PrenotazioneBase):
# user_id will be extracted from JWT token
pass
class PrenotazioneUpdate(BaseModel):
pietanze_selezionate: Optional[List[int]] = None
note: Optional[str] = Field(None, max_length=500)
stato: Optional[StatoPrenotazione] = None
@validator('pietanze_selezionate')
def validate_pietanze_selezionate(cls, v):
if v is None:
return v
if len(v) == 0:
raise ValueError("Almeno una pietanza deve essere selezionata")
if len(v) != len(set(v)):
raise ValueError("Non sono ammesse pietanze duplicate")
for pietanza_id in v:
if not isinstance(pietanza_id, int) or pietanza_id <= 0:
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
return v
class PrenotazioneResponse(PrenotazioneBase):
id: int
user_id: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True