This commit is contained in:
adii1823
2021-10-28 17:41:38 +05:30
parent 03bb4d43c0
commit 0a0ceaf7d7
51 changed files with 2945 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# api_code/.env
SECRET_KEY="ec604d5610ac4668a44418711be8251f"
DEBUG=false
API_VERSION=1.0.0
+1
View File
@@ -0,0 +1 @@
# api_code/api/__init__.py
+42
View File
@@ -0,0 +1,42 @@
# api_code/api/admin.py
from typing import Optional
from fastapi import (
APIRouter,
Depends,
Header,
HTTPException,
Response,
status,
)
from sqlalchemy.orm import Session
from . import crud
from .deps import Settings, get_db, get_settings
from .util import is_admin
router = APIRouter(prefix="/admin")
def ensure_admin(settings: Settings, authorization: str):
if not is_admin(
settings=settings, authorization=authorization
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"You must be an admin to access this endpoint.",
)
@router.delete("/stations/{station_id}", tags=["Admin"])
def admin_delete_station(
station_id: int,
authorization: Optional[str] = Header(None),
settings: Settings = Depends(get_settings),
db: Session = Depends(get_db),
):
ensure_admin(settings, authorization)
row_count = crud.delete_station(db=db, station_id=station_id)
if row_count:
return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(status_code=status.HTTP_404_NOT_FOUND)
+11
View File
@@ -0,0 +1,11 @@
# api_code/api/config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
secret_key: str
debug: bool
api_version: str
class Config:
env_file = ".env"
+231
View File
@@ -0,0 +1,231 @@
# api_code/api/crud.py
from datetime import datetime, timezone
from sqlalchemy import delete, update
from sqlalchemy.orm import Session, aliased
from . import models, schemas
# USERS
def get_users(db: Session, email: str = None):
q = db.query(models.User)
if email is not None:
q = q.filter(models.User.email.ilike(f"%{email}%"))
return q.all()
def get_user(db: Session, user_id: int):
return (
db.query(models.User)
.filter(models.User.id == user_id)
.first()
)
def get_user_by_email(db: Session, email: str):
return (
db.query(models.User)
.filter(models.User.email.ilike(email))
.first()
)
def create_user(
db: Session, user: schemas.UserCreate, user_id: int = None
):
hashed_password = models.User.hash_password(user.password)
user_dict = {
**user.dict(exclude_unset=True),
"password": hashed_password,
}
if user_id is not None:
user_dict.update({"id": user_id})
db_user = models.User(**user_dict)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def update_user(
db: Session, user: schemas.UserUpdate, user_id: int
):
user_dict = {
**user.dict(exclude_unset=True),
}
if user.password is not None:
user_dict.update(
{"password": models.User.hash_password(user.password)}
)
stm = (
update(models.User)
.where(models.User.id == user_id)
.values(user_dict)
)
result = db.execute(stm)
db.commit()
return result.rowcount
def delete_user(db: Session, user_id: int):
stm = delete(models.User).where(models.User.id == user_id)
result = db.execute(stm)
db.commit()
return result.rowcount
# STATIONS
def get_stations(db: Session, code: str = None):
q = db.query(models.Station)
if code is not None:
q = q.filter(models.Station.code.ilike(code))
return q.all()
def get_station(db: Session, station_id: int):
return (
db.query(models.Station)
.filter(models.Station.id == station_id)
.first()
)
def get_station_by_code(db: Session, code: str):
return (
db.query(models.Station)
.filter(models.Station.code.ilike(code))
.first()
)
def create_station(
db: Session,
station: schemas.StationCreate,
):
db_station = models.Station(**station.dict())
db.add(db_station)
db.commit()
db.refresh(db_station)
return db_station
def update_station(
db: Session, station: schemas.StationUpdate, station_id: int
):
stm = (
update(models.Station)
.where(models.Station.id == station_id)
.values(station.dict(exclude_unset=True))
)
result = db.execute(stm)
db.commit()
return result.rowcount
def delete_station(db: Session, station_id: int):
stm = delete(models.Station).where(
models.Station.id == station_id
)
result = db.execute(stm)
db.commit()
return result.rowcount
# TRAINS
def get_trains(
db: Session,
station_from_code: str = None,
station_to_code: str = None,
include_all: bool = False,
):
q = db.query(models.Train)
if station_from_code is not None:
st_from = aliased(models.Station)
q = q.join(st_from, models.Train.station_from)
q = q.filter(st_from.code.ilike(f"%{station_from_code}%"))
if station_to_code is not None:
st_to = aliased(models.Station)
q = q.join(st_to, models.Train.station_to)
q = q.filter(st_to.code.ilike(f"%{station_to_code}%"))
if not include_all:
now = datetime.now(tz=timezone.utc)
q = q.filter(models.Train.departs_at > now)
return q.all()
def get_train(db: Session, train_id: int):
return (
db.query(models.Train)
.filter(models.Train.id == train_id)
.first()
)
def get_train_by_name(db: Session, name: str):
return (
db.query(models.Train)
.filter(models.Train.name == name)
.first()
)
def create_train(db: Session, train: schemas.TrainCreate):
train_dict = train.dict(exclude_unset=True)
db_train = models.Train(**train_dict)
db.add(db_train)
db.commit()
db.refresh(db_train)
return db_train
def delete_train(db: Session, train_id: int):
stm = delete(models.Train).where(models.Train.id == train_id)
result = db.execute(stm)
db.commit()
return result.rowcount
# TICKETS
def get_tickets(db: Session):
return db.query(models.Ticket).all()
def get_ticket(db: Session, ticket_id: int):
return (
db.query(models.Ticket)
.filter(models.Ticket.id == ticket_id)
.first()
)
def create_ticket(db: Session, ticket: schemas.TicketCreate):
ticket_dict = ticket.dict(exclude_unset=True)
ticket_dict.update(
{"created_at": datetime.now(tz=timezone.utc)}
)
db_ticket = models.Ticket(**ticket_dict)
db.add(db_ticket)
db.commit()
db.refresh(db_ticket)
return db_ticket
def delete_ticket(db: Session, ticket_id: int):
stm = delete(models.Ticket).where(
models.Ticket.id == ticket_id
)
result = db.execute(stm)
db.commit()
return result.rowcount
+24
View File
@@ -0,0 +1,24 @@
# api_code/api/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .config import Settings
settings = Settings()
DB_URL = "sqlite:///train.db"
engine = create_engine(
DB_URL,
connect_args={"check_same_thread": False},
echo=settings.debug, # when debug is True, queries are logged
)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine
)
Base = declarative_base()
+20
View File
@@ -0,0 +1,20 @@
# api_code/api/deps.py
from functools import lru_cache
from .config import Settings
from .database import SessionLocal
def get_db():
"""Return a DB Session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
@lru_cache
def get_settings():
"""Return the app settings."""
return Settings()
+181
View File
@@ -0,0 +1,181 @@
# api_code/api/models.py
import enum
import hashlib
import os
import secrets
from sqlalchemy import (
Column,
DateTime,
Enum,
Float,
ForeignKey,
Integer,
Unicode,
)
from sqlalchemy.orm import relationship
from .database import Base
UNICODE_LEN = 128
SALT_LEN = 64
# Enums
class Classes(str, enum.Enum):
first = "first"
second = "second"
class Roles(str, enum.Enum):
admin = "admin"
passenger = "passenger"
# Models
class Station(Base):
__tablename__ = "station"
id = Column(Integer, primary_key=True)
code = Column(
Unicode(UNICODE_LEN), nullable=False, unique=True
)
country = Column(Unicode(UNICODE_LEN), nullable=False)
city = Column(Unicode(UNICODE_LEN), nullable=False)
departures = relationship(
"Train",
foreign_keys="[Train.station_from_id]",
back_populates="station_from",
)
arrivals = relationship(
"Train",
foreign_keys="[Train.station_to_id]",
back_populates="station_to",
)
def __repr__(self):
return f"<{self.code}: id={self.id} city={self.city}>"
__str__ = __repr__
class Train(Base):
__tablename__ = "train"
id = Column(Integer, primary_key=True)
name = Column(Unicode(UNICODE_LEN), nullable=False)
station_from_id = Column(
ForeignKey("station.id"), nullable=False
)
station_from = relationship(
"Station",
foreign_keys=[station_from_id],
back_populates="departures",
)
station_to_id = Column(
ForeignKey("station.id"), nullable=False
)
station_to = relationship(
"Station",
foreign_keys=[station_to_id],
back_populates="arrivals",
)
departs_at = Column(DateTime(timezone=True), nullable=False)
arrives_at = Column(DateTime(timezone=True), nullable=False)
first_class = Column(Integer, default=0, nullable=False)
second_class = Column(Integer, default=0, nullable=False)
seats_per_car = Column(Integer, default=0, nullable=False)
tickets = relationship("Ticket", back_populates="train")
def __repr__(self):
return f"<{self.name}: id={self.id}>"
__str__ = __repr__
class Ticket(Base):
__tablename__ = "ticket"
id = Column(Integer, primary_key=True)
created_at = Column(DateTime(timezone=True), nullable=False)
user_id = Column(ForeignKey("user.id"), nullable=False)
user = relationship(
"User", foreign_keys=[user_id], back_populates="tickets"
)
train_id = Column(ForeignKey("train.id"), nullable=False)
train = relationship(
"Train", foreign_keys=[train_id], back_populates="tickets"
)
price = Column(Float, default=0, nullable=False)
car_class = Column(Enum(Classes), nullable=False)
def __repr__(self):
return (
f"<id={self.id} user={self.user} train={self.train}>"
)
__str__ = __repr__
class User(Base):
__tablename__ = "user"
pwd_separator = "#"
id = Column(Integer, primary_key=True)
full_name = Column(Unicode(UNICODE_LEN), nullable=False)
email = Column(Unicode(256), nullable=False, unique=True)
password = Column(Unicode(256), nullable=False)
role = Column(Enum(Roles), nullable=False)
tickets = relationship("Ticket", back_populates="user")
def is_valid_password(self, password: str):
"""Tell if password matches the one stored in DB."""
salt, stored_hash = self.password.split(
self.pwd_separator
)
_, computed_hash = _hash(
password=password, salt=bytes.fromhex(salt)
)
return secrets.compare_digest(stored_hash, computed_hash)
@classmethod
def hash_password(cls, password: str, salt: bytes = None):
salt, hashed = _hash(password=password, salt=salt)
return f"{salt}{cls.pwd_separator}{hashed}"
def __repr__(self):
return (
f"<{self.full_name}: id={self.id} "
f"role={self.role.name}>"
)
__str__ = __repr__
def _hash(password: str, salt: bytes = None):
if salt is None:
salt = os.urandom(SALT_LEN)
iterations = 100 # should be at least 100k for SHA-256
hashed = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
salt,
iterations,
dklen=128,
)
return salt.hex(), hashed.hex()
+118
View File
@@ -0,0 +1,118 @@
# api_code/api/schemas.py
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
from . import models
# USERS
class Auth(BaseModel):
email: str
password: str
class AuthToken(BaseModel):
token: str
class UserBase(BaseModel):
full_name: str
email: str
role: models.Roles
class User(UserBase):
id: int
class Config:
orm_mode = True
use_enum_values = True
class UserCreate(UserBase):
password: str
class UserUpdate(UserBase):
full_name: Optional[str] = None
email: Optional[str] = None
password: Optional[str] = None
role: Optional[models.Roles] = None
# STATIONS
class StationBase(BaseModel):
code: str
country: str
city: str
class Station(StationBase):
id: int
class Config:
orm_mode = True
class StationCreate(StationBase):
pass
class StationUpdate(StationBase):
code: Optional[str] = None
country: Optional[str] = None
city: Optional[str] = None
# TRAINS
class TrainBase(BaseModel):
name: str
station_from: Optional[Station] = None
station_to: Optional[Station] = None
departs_at: datetime
arrives_at: datetime
first_class: int
second_class: int
seats_per_car: int
class Train(TrainBase):
id: int
class Config:
orm_mode = True
class TrainCreate(TrainBase):
station_from_id: int
station_to_id: int
# TICKETS
class TicketBase(BaseModel):
user_id: int
train_id: int
price: float
car_class: models.Classes
class Ticket(TicketBase):
id: int
created_at: datetime
class Config:
orm_mode = True
use_enum_values = True
class TicketCreate(TicketBase):
pass
+122
View File
@@ -0,0 +1,122 @@
# api_code/api/stations.py
from typing import Optional
from fastapi import (
APIRouter,
Depends,
HTTPException,
Response,
status,
)
from sqlalchemy.orm import Session
from . import crud
from .deps import get_db
from .schemas import Station, StationCreate, StationUpdate, Train
router = APIRouter(prefix="/stations")
@router.get("", response_model=list[Station], tags=["Stations"])
def get_stations(
db: Session = Depends(get_db), code: Optional[str] = None
):
return crud.get_stations(db=db, code=code)
@router.get(
"/{station_id}", response_model=Station, tags=["Stations"]
)
def get_station(station_id: int, db: Session = Depends(get_db)):
db_station = crud.get_station(db=db, station_id=station_id)
if db_station is None:
raise HTTPException(
status_code=404,
detail=f"Station {station_id} not found.",
)
return db_station
@router.get(
"/{station_id}/departures",
response_model=list[Train],
tags=["Trains"],
)
def get_station_departures(
station_id: int, db: Session = Depends(get_db)
):
db_station = _get_station(db=db, station_id=station_id)
return db_station.departures
def _get_station(db: Session, station_id: int):
db_station = crud.get_station(db=db, station_id=station_id)
if db_station is None:
raise HTTPException(
status_code=404,
detail=f"Station {station_id} not found.",
)
return db_station
@router.get(
"/{station_id}/arrivals",
response_model=list[Train],
tags=["Trains"],
)
def get_station_arrivals(
station_id: int, db: Session = Depends(get_db)
):
db_station = _get_station(db=db, station_id=station_id)
return db_station.arrivals
@router.post(
"",
response_model=Station,
status_code=status.HTTP_201_CREATED,
tags=["Stations"],
)
def create_station(
station: StationCreate, db: Session = Depends(get_db)
):
db_station = crud.get_station_by_code(
db=db, code=station.code
)
if db_station:
raise HTTPException(
status_code=400,
detail=f"Station {station.code} already exists.",
)
return crud.create_station(db=db, station=station)
@router.put("/{station_id}", tags=["Stations"])
def update_station(
station_id: int,
station: StationUpdate,
db: Session = Depends(get_db),
):
db_station = crud.get_station(db=db, station_id=station_id)
if db_station is None:
raise HTTPException(
status_code=404,
detail=f"Station {station_id} not found.",
)
else:
crud.update_station(
db=db, station=station, station_id=station_id
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{station_id}", tags=["Stations"])
def delete_station(
station_id: int, db: Session = Depends(get_db)
):
row_count = crud.delete_station(db=db, station_id=station_id)
if row_count:
return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(status_code=status.HTTP_404_NOT_FOUND)
+53
View File
@@ -0,0 +1,53 @@
# api_code/api/tickets.py
from fastapi import (
APIRouter,
Depends,
HTTPException,
Response,
status,
)
from sqlalchemy.orm import Session
from . import crud
from .deps import get_db
from .schemas import Ticket, TicketCreate
router = APIRouter(prefix="/tickets")
@router.get("", response_model=list[Ticket], tags=["Tickets"])
def get_tickets(db: Session = Depends(get_db)):
return crud.get_tickets(db=db)
@router.get(
"/{ticket_id}", response_model=Ticket, tags=["Tickets"]
)
def get_ticket(ticket_id: int, db: Session = Depends(get_db)):
db_ticket = crud.get_ticket(db=db, ticket_id=ticket_id)
if db_ticket is None:
raise HTTPException(
status_code=404,
detail=f"Ticket {ticket_id} not found.",
)
return db_ticket
@router.post(
"",
response_model=Ticket,
status_code=status.HTTP_201_CREATED,
tags=["Tickets"],
)
def create_ticket(
ticket: TicketCreate, db: Session = Depends(get_db)
):
return crud.create_ticket(db=db, ticket=ticket)
@router.delete("/{ticket_id}", tags=["Tickets"])
def delete_ticket(ticket_id: int, db: Session = Depends(get_db)):
row_count = crud.delete_ticket(db=db, ticket_id=ticket_id)
if row_count:
return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(status_code=status.HTTP_404_NOT_FOUND)
+84
View File
@@ -0,0 +1,84 @@
# api_code/api/trains.py
from typing import Optional
from fastapi import (
APIRouter,
Depends,
HTTPException,
Response,
status,
)
from sqlalchemy.orm import Session
from . import crud
from .deps import get_db
from .schemas import Ticket, Train, TrainCreate
router = APIRouter(prefix="/trains")
@router.get("", response_model=list[Train], tags=["Trains"])
def get_trains(
db: Session = Depends(get_db),
station_from_code: str = None,
station_to_code: str = None,
include_all: Optional[bool] = False,
):
return crud.get_trains(
db=db,
station_from_code=station_from_code,
station_to_code=station_to_code,
include_all=include_all,
)
@router.get("/{train_id}", response_model=Train, tags=["Trains"])
def get_train(train_id: int, db: Session = Depends(get_db)):
db_train = crud.get_train(db=db, train_id=train_id)
if db_train is None:
raise HTTPException(
status_code=404, detail=f"Train {train_id} not found."
)
return db_train
@router.get(
"/{train_id}/tickets",
response_model=list[Ticket],
tags=["Tickets"],
)
def get_train_tickets(
train_id: int, db: Session = Depends(get_db)
):
db_train = crud.get_train(db=db, train_id=train_id)
if db_train is None:
raise HTTPException(
status_code=404, detail=f"Train {train_id} not found."
)
return db_train.tickets
@router.post(
"",
response_model=Train,
status_code=status.HTTP_201_CREATED,
tags=["Trains"],
)
def create_train(
train: TrainCreate, db: Session = Depends(get_db)
):
db_train = crud.get_train_by_name(db=db, name=train.name)
if db_train:
raise HTTPException(
status_code=400,
detail=f"Train {train.name} already exists.",
)
return crud.create_train(db=db, train=train)
@router.delete("/{train_id}", tags=["Trains"])
def delete_user(train_id: int, db: Session = Depends(get_db)):
row_count = crud.delete_train(db=db, train_id=train_id)
if row_count:
return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(status_code=status.HTTP_404_NOT_FOUND)
+146
View File
@@ -0,0 +1,146 @@
# api_code/api/users.py
from typing import Optional
from fastapi import (
APIRouter,
Depends,
HTTPException,
Response,
status,
)
from sqlalchemy.orm import Session
from . import crud
from .deps import Settings, get_db, get_settings
from .schemas import (
Auth,
AuthToken,
Ticket,
User,
UserCreate,
UserUpdate,
)
from .util import InvalidToken, create_token, extract_payload
router = APIRouter(prefix="/users")
@router.get("", response_model=list[User], tags=["Users"])
def get_users(
db: Session = Depends(get_db), email: Optional[str] = None
):
return crud.get_users(db=db, email=email)
@router.get("/{user_id}", response_model=User, tags=["Users"])
def get_user(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user(db=db, user_id=user_id)
if db_user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} not found.",
)
return db_user
@router.get(
"/{user_id}/tickets",
response_model=list[Ticket],
tags=["Users"],
)
def get_user_tickets(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user(db=db, user_id=user_id)
if db_user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} not found.",
)
return db_user.tickets
@router.post(
"",
response_model=User,
status_code=status.HTTP_201_CREATED,
tags=["Users"],
)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db=db, email=user.email)
if db_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"User {user.email} already exists.",
)
return crud.create_user(db=db, user=user)
@router.put("/{user_id}", response_model=User, tags=["Users"])
def update_user(
user_id: int, user: UserUpdate, db: Session = Depends(get_db)
):
db_user = crud.get_user(db=db, user_id=user_id)
if db_user is None:
db_user = crud.get_user_by_email(db, user.email)
if db_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"User {user.email} already exists.",
)
else:
crud.create_user(db=db, user=user, user_id=user_id)
return Response(status_code=status.HTTP_201_CREATED)
else:
crud.update_user(db=db, user=user, user_id=user_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{user_id}", tags=["Users"])
def delete_user(user_id: int, db: Session = Depends(get_db)):
row_count = crud.delete_user(db=db, user_id=user_id)
if row_count:
return Response(status_code=status.HTTP_204_NO_CONTENT)
return Response(status_code=status.HTTP_404_NOT_FOUND)
@router.post("/authenticate", tags=["Auth"])
def authenticate(
auth: Auth,
db: Session = Depends(get_db),
settings: Settings = Depends(get_settings),
):
db_user = crud.get_user_by_email(db=db, email=auth.email)
if db_user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {auth.email} not found.",
)
if not db_user.is_valid_password(auth.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Wrong username/password.",
)
payload = {
"email": auth.email,
"role": db_user.role.value,
}
return create_token(payload, settings.secret_key)
@router.post("/validate_token", tags=["Auth"])
def validate_token(
auth: AuthToken,
settings: Settings = Depends(get_settings),
):
try:
return extract_payload(auth.token, settings.secret_key)
except InvalidToken as err:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid token: {err}",
)
+54
View File
@@ -0,0 +1,54 @@
# api_code/api/util.py
from datetime import datetime, timedelta, timezone
from typing import Optional
import jwt
from jwt.exceptions import PyJWTError
from .deps import Settings
ALGORITHM = "HS256"
class InvalidToken(Exception):
pass
def create_token(payload: dict, key: str):
now = datetime.now(tz=timezone.utc)
data = {
"iat": now,
"exp": now + timedelta(hours=24),
**payload,
}
return jwt.encode(data, key, algorithm=ALGORITHM)
def extract_payload(token: str, key: str):
try:
return jwt.decode(token, key, algorithms=[ALGORITHM])
except PyJWTError as err:
raise InvalidToken(str(err))
def is_admin(
settings: Settings, authorization: Optional[str] = None
):
if authorization is None:
return False
partition_key = (
"Bearer" if "Bearer" in authorization else "bearer"
)
*dontcare, token = authorization.partition(
f"{partition_key} "
)
token = token.strip()
try:
payload = extract_payload(token, settings.secret_key)
except InvalidToken:
return False
else:
return payload.get("role") == "admin"
+182
View File
@@ -0,0 +1,182 @@
# api_code/dummy_data.py
from datetime import datetime, timedelta, timezone
from pathlib import Path
from random import choice, randint, random
from api.models import (
Base,
Classes,
Roles,
Station,
Ticket,
Train,
User,
)
from faker import Faker
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DB_URL = "sqlite:///train.db"
engine = create_engine(DB_URL)
def new_db(filename):
db_file = Path(filename)
db_file.unlink(missing_ok=True)
# then create a fresh DB
Base.metadata.create_all(engine)
if __name__ == "__main__":
new_db("train.db")
Session = sessionmaker(bind=engine)
session = Session()
fake = Faker()
# USERS
NUM_USERS = 100
NUM_TICKETS = 300
NUM_TRAINS = 300
class_choices = [c for c in Classes]
users = [
User(
id=0,
full_name="Fabrizio Romano",
email="fabrizio.romano@example.com",
password=User.hash_password("f4bPassword"),
role=Roles.admin,
)
]
for user_id in range(1, NUM_USERS + 1):
users.append(
User(
id=user_id,
full_name=fake.name(),
email=fake.safe_email(),
password=User.hash_password(fake.password()),
role=Roles.passenger,
)
)
session.bulk_save_objects(users)
session.commit()
# STATIONS
stations = [
Station(id=0, code="ROM", country="Italy", city="Rome"),
Station(id=1, code="PAR", country="France", city="Paris"),
Station(id=2, code="LDN", country="UK", city="London"),
Station(id=3, code="KYV", country="Ukraine", city="Kyiv"),
Station(
id=4, code="STK", country="Sweden", city="Stockholm"
),
Station(
id=5, code="WSW", country="Poland", city="Warsaw"
),
Station(
id=6, code="MSK", country="Russia", city="Moskow"
),
Station(
id=7,
code="AMD",
country="Netherlands",
city="Amsterdam",
),
Station(
id=8, code="EDB", country="Scotland", city="Edinburgh"
),
Station(
id=9, code="BDP", country="Hungary", city="Budapest"
),
Station(
id=10, code="BCR", country="Romania", city="Bucharest"
),
Station(
id=11, code="SFA", country="Bulgaria", city="Sofia"
),
]
session.bulk_save_objects(stations)
session.commit()
# TRAINS
trains = []
now = datetime.now(tz=timezone.utc)
HOUR = 60 * 60
DAY = 24 * HOUR
TEN_DAYS = 10 * DAY
for train_id in range(NUM_TRAINS):
station_ids = list(range(len(stations)))
station_from = choice(station_ids)
station_ids.remove(station_from)
station_to = choice(station_ids)
name = f"{stations[station_from].city} -> {stations[station_to].city}"
departure = now + timedelta(
seconds=randint(-TEN_DAYS, TEN_DAYS)
)
arrival = departure + timedelta(
seconds=randint(HOUR, DAY)
)
trains.append(
Train(
id=train_id,
name=name,
station_from_id=station_from,
station_to_id=station_to,
departs_at=departure,
arrives_at=arrival,
first_class=randint(0, 5),
second_class=randint(1, 10),
seats_per_car=choice([10, 25, 40]),
)
)
session.bulk_save_objects(trains)
session.commit()
# TICKETS
tickets = []
classes = [c for c in Classes]
MIN_PRICE = 5
MAX_PRICE = 200
for ticket_id in range(NUM_TICKETS):
price = round(
float(randint(MIN_PRICE, MAX_PRICE))
+ randint(0, 1) * random(),
2,
)
tickets.append(
Ticket(
id=ticket_id,
created_at=now
+ timedelta(seconds=randint(-TEN_DAYS, -HOUR)),
user_id=choice(
range(len(users) - 1)
), # last user has no tickets
train_id=choice(
range(len(trains) - 1)
), # last train has no tickets
price=price,
car_class=choice(classes),
)
)
session.bulk_save_objects(tickets)
session.commit()
print("done")
+20
View File
@@ -0,0 +1,20 @@
# api_code/main.py
from api import admin, config, stations, tickets, trains, users
from fastapi import FastAPI
settings = config.Settings()
app = FastAPI()
app.include_router(admin.router)
app.include_router(stations.router)
app.include_router(trains.router)
app.include_router(users.router)
app.include_router(tickets.router)
@app.get("/")
def root():
return {
"message": f"Welcome to version {settings.api_version} of our API"
}
+125
View File
@@ -0,0 +1,125 @@
# HTTP queries
These are example queries that exercise the API.
Try all of them, especially those that create or delete resources,
should be tried twice, consecutively, so you can also see the error
messages that the API will return.
*Note*: You must install [httpie](https://httpie.io) to run the
queries below.
## Root
### GET
http http://localhost:8000
## Users
### GET
http http://localhost:8000/users
http http://localhost:8000/users/0
http http://localhost:8000/users/0/tickets
### POST
http POST http://localhost:8000/users full_name="John Doe" email="john.doe@example.com" password="johndoe" role="passenger"
http POST http://localhost:8000/users/authenticate email="fabrizio.romano@example.com" password="f4bPassword"
http POST http://localhost:8000/users/validate_token token="..."
### PUT
http PUT http://localhost:8000/users/101 full_name="Fabrizio Romano" email="fab109@example.com" password="something" role="admin"
Also available partial updates:
http PUT http://localhost:8000/users/101 role="passenger"
### DELETE
http DELETE http://localhost:8000/users/101
## Stations
### GET
http http://localhost:8000/stations
http http://localhost:8000/stations?code=LDN
http http://localhost:8000/stations/0
http http://localhost:8000/stations/0/departures
http http://localhost:8000/stations/0/arrivals
### POST
http POST http://localhost:8000/stations code=TMP country=Temporary-Country city=tmp-city
### PUT
http PUT http://localhost:8000/stations/12 code=SMC country=Some-Country city=Some-city
Also available partial updates:
http PUT http://localhost:8000/stations/12 code=xxx
### DELETE
http DELETE http://localhost:8000/stations/12
## Trains
### GET
http http://localhost:8000/trains
http http://localhost:8000/trains?station_from_code=BCR
http http://localhost:8000/trains?station_to_code=STK
http "http://localhost:8000/trains?station_from_code=STK&station_to_code=AMD"
http "http://localhost:8000/trains?station_from_code=STK&station_to_code=AMD&include_all=True"
http http://localhost:8000/trains/0
### POST
http POST http://localhost:8000/trains name="Pendolino" first_class=2 second_class=4 seats_per_car=8 station_from_id=0 station_to_id=3 arrives_at="2021-08-18T11:33:20" departs_at="2021-08-18T09:55:20"
### DELETE
http DELETE http://localhost:8000/trains/300
## Tickets
### GET
http http://localhost:8000/tickets
http http://localhost:8000/tickets/0
### POST
http POST http://localhost:8000/tickets user_id=0 train_id=0 price=19.84 car_class="first"
### DELETE
http DELETE http://localhost:8000/tickets/300
## Admin
### GET
http DELETE http://localhost:8000/admin/stations/10 Authorization:"Bearer admin.token.here"
Binary file not shown.