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
+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"