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

56
ch14/README.md Normal file
View File

@ -0,0 +1,56 @@
# CH14 - Readme
This chapter is about APIs. You will find three main folders:
1. `api_code` contains the FastAPI project about Railways
2. `apic` or API Consumer. This is a Django project to show an example on how to talk to an API from another application.
3. `samples`. This folder contains the examples that went in the chapter, so you can ignore it.
## Setup
Install requirements with pip from their folder:
$ pip install -r requirements.txt
If you want to create your own dummy data, please also install the dev requirements:
$ pip install -r dev.txt
To generate a new database with random data:
$ cd api_code
$ python dummy_data.py
This will generate a new `train.db` file for you.
## Running the API
Open a terminal window, change into the `api_code` folder and type this command:
$ uvicorn main:app --reload
The `--reload` flag makes sure uvicorn is reloaded if you make changes to the
source while the app is running.
## Running the Django consumer
While the API is running in a terminal, open another terminal window,
change into the `apic` folder, and type the following:
$ python manage.py migrate # only the first time, to generate the django db
Once migrations have been applied, run this to start the app:
$ python manage.py runserver 8080
We need to specify a port for Django that is not 8000 (its normal default),
since the API is running on that one.
When the Django app is running, open a browser and go to: http://localhost:8080
You should see the welcome page of the Railways app. From there you can navigate using links.
In the authentication section, you can use these credentials to authenticate:
username: fabrizio.romano@example.com
password: f4bPassword

4
ch14/api_code/.env Normal file
View File

@ -0,0 +1,4 @@
# api_code/.env
SECRET_KEY="ec604d5610ac4668a44418711be8251f"
DEBUG=false
API_VERSION=1.0.0

View File

@ -0,0 +1 @@
# api_code/api/__init__.py

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)

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
ch14/api_code/api/crud.py Normal file
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

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
ch14/api_code/api/deps.py Normal file
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
ch14/api_code/api/models.py Normal file
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()

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

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)

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)

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
ch14/api_code/api/users.py Normal file
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
ch14/api_code/api/util.py Normal file
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
ch14/api_code/dummy_data.py Normal file
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
ch14/api_code/main.py Normal file
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
ch14/api_code/queries.md Normal file
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"

BIN
ch14/api_code/train.db Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
# apic/apic/__init__.py

17
ch14/apic/apic/asgi.py Normal file
View File

@ -0,0 +1,17 @@
# apic/apic/asgi.py
"""
ASGI config for apic project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apic.settings")
application = get_asgi_application()

132
ch14/apic/apic/settings.py Normal file
View File

@ -0,0 +1,132 @@
# apic/apic/settings.py
"""
Django settings for apic project.
Generated by 'django-admin startproject' using Django 3.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-98u5ntukr@mo0e5c*ve+8bk5$i3lr+4n4gc^@=b-7c*j_lyxa("
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"rails.apps.RailsConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "apic.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "apic.wsgi.application"
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = "/static/"
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# API Settings
BASE_API_URL = "http://localhost:8000"

23
ch14/apic/apic/urls.py Normal file
View File

@ -0,0 +1,23 @@
# apic/apic/urls.py
"""apic URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("rails.urls")),
path("admin/", admin.site.urls),
]

17
ch14/apic/apic/wsgi.py Normal file
View File

@ -0,0 +1,17 @@
# apic/apic/wsgi.py
"""
WSGI config for apic project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apic.settings")
application = get_wsgi_application()

27
ch14/apic/manage.py Normal file
View File

@ -0,0 +1,27 @@
# apic/manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "apic.settings"
)
try:
from django.core.management import (
execute_from_command_line,
)
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@
# apic/rails/__init__.py

4
ch14/apic/rails/admin.py Normal file
View File

@ -0,0 +1,4 @@
# apic/rails/admin.py
from django.contrib import admin
# Register your models here.

7
ch14/apic/rails/apps.py Normal file
View File

@ -0,0 +1,7 @@
# apic/rails/apps.py
from django.apps import AppConfig
class RailsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "rails"

9
ch14/apic/rails/forms.py Normal file
View File

@ -0,0 +1,9 @@
# apic/rails/forms.py
from django import forms
class AuthenticateForm(forms.Form):
email = forms.EmailField(max_length=256, label="Username")
password = forms.CharField(
label="Password", widget=forms.PasswordInput
)

View File

@ -0,0 +1 @@
# apic/rails/migrations/__init__.py

View File

@ -0,0 +1,4 @@
# apic/rails/models.py
from django.db import models
# Create your models here.

View File

@ -0,0 +1,29 @@
body {
font-family: "Georgia", Times, serif;
}
.bg_color1 {
background-color: lightsteelblue;
padding: 0.1em 0 0.1em 0.8em;
}
.bg_color2 {
background-color:whitesmoke;
padding: 0.1em 0 0.1em 0.8em;
}
.footer {
padding-top: 1em;
}
a {
color: #333;
}
a:hover {
color:steelblue;
}
.error {
color:darkred;
}

View File

@ -0,0 +1,54 @@
{% extends "rails/base.html" %}
{% block title %}Arrivals{% endblock %}
{% block content %}
{% if arrivals %}
<h1>Arrivals to {{ arrivals.0.station_to.city }} ({{ arrivals.0.station_to.code }})</h1>
{% endif %}
{% for arv in arrivals %}
<div class="{% cycle 'bg_color1' 'bg_color2' %}">
<h3>{{ arv.name }}</h3>
<p>
<em>Departs at</em>: {{ arv.departs_at }}<br>
<em>Arrives at</em>: {{ arv.arrives_at }}<br>
<em>Cars</em>: {{ arv.first_class}} FC,
{{ arv.second_class }} SC
({{ arv.seats_per_car }} seats/car)
</p>
</div>
{% empty %}
{% if error %}
<div class=" error">
<h3>Error</h3>
<p>There was a problem connecting to the API.</p>
<code>{{ error }}</code>
<p>
(<em>The above error is shown to the user as an example.
For security reasons these errors are normally hidden from the user</em>)
</p>
</div>
{% else %}
<div>
<p>There are no arrivals available at this time.</p>
</div>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
<div class="footer">
<a href="{% url 'stations' %}">Back to stations</a>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "rails/base.html" %}
{% block title %}Authentication{% endblock %}
{% block content %}
<h1>Authentication</h1>
<form action="{% url 'authenticate' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Authenticate">
</form>
{% endblock %}
{% block footer %}
<div class="footer">
<a href="{% url 'index' %}">Home</a>
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "rails/base.html" %}
{% block title %}Authentication Result{% endblock %}
{% block content %}
{% if token %}
<p>
<label for="token">
<h3>Your JWT token:</h3>
</label>
</p>
<textarea id="token" name="token" rows="5" cols="60">
{{ token }}
</textarea>
{% else %}
<p>We were unable to retrieve your token.</p>
{% if auth_error %}
<code class="error">{{ auth_error }}</code>
{% endif %}
{% endif %}
{% endblock %}
{% block footer %}
<div class="footer">
<a href="{% url 'authenticate' %}">Back to authenticate</a>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>{% block title %}Hello{% endblock %}</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" type="text/css" href="{% static 'rails/style.css' %}">
<body>
{% block content %}
{% endblock %}
{% block footer %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,53 @@
{% extends "rails/base.html" %}
{% block title %}Departures{% endblock %}
{% block content %}
{% if departures %}
<h1>Departures from {{ departures.0.station_from.city }} ({{ departures.0.station_from.code }})</h1>
{% endif %}
{% for dep in departures %}
<div class="{% cycle 'bg_color1' 'bg_color2' %}">
<h3>{{ dep.name }}</h3>
<p>
<em>Departs at</em>: {{ dep.departs_at }}<br>
<em>Arrives at</em>: {{ dep.arrives_at }}<br>
<em>Cars</em>: {{ dep.first_class}} FC,
{{ dep.second_class }} SC
({{ dep.seats_per_car }} seats/car)
</p>
</div>
{% empty %}
{% if error %}
<div class=" error">
<h3>Error</h3>
<p>There was a problem connecting to the API.</p>
<code>{{ error }}</code>
<p>
(<em>The above error is shown to the user as an example.
For security reasons these errors are normally hidden from the user</em>)
</p>
</div>
{% else %}
<div>
<p>There are no departures available at this time.</p>
</div>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
<div class="footer">
<a href="{% url 'stations' %}">Back to stations</a>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "rails/base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Wecome to our Railways website!</h1>
<p>Please choose which page you wish to visit below.</p>
<p>
<a href="{% url 'stations' %}">Stations</a>
</p>
<p>
<a href="{% url 'users' %}">Users</a>
</p>
<p>
<a href="{% url 'authenticate' %}">Authentication</a>
</p>
{% endblock %}
{% block footer %}{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "rails/base.html" %}
{% block title %}Stations{% endblock %}
{% block content %}
{% if stations %}
<h1>Stations</h1>
{% endif %}
{% for station in stations %}
<div class="{% cycle 'bg_color1' 'bg_color2' %}">
<p>Id: {{ station.id }} &lt;Code: {{ station.code }}
({{ station.city }}, {{ station.country }})&gt;&nbsp;
<a href="{% url 'departures' station.id %}">Departures</a> -
<a href="{% url 'arrivals' station.id %}"">Arrivals</a>
</p>
</div>
{% empty %}
{% if error %}
<div class=" error">
<h3>Error</h3>
<p>There was a problem connecting to the API.</p>
<code>{{ error }}</code>
<p>
(<em>The above error is shown to the user as an example.
For security reasons these errors are normally hidden
from the user</em>)
</p>
</div>
{% else %}
<div>
<p>There are no stations available at this time.</p>
</div>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
<div class="footer">
<a href="{% url 'index' %}">Home</a>
</div>
{% endblock %}

View File

@ -0,0 +1,50 @@
{% extends "rails/base.html" %}
{% block title %}Users{% endblock %}
{% block content %}
{% if users %}
<h1>Users</h1>
{% endif %}
{% for user in users %}
<div class="{% cycle 'bg_color1' 'bg_color2' %}">
<p>Id: {{ user.id }} &lt;{{ user.full_name }}
(<a href="mailto: {{ user.email }})">{{ user.email }}</a>&gt;
{{ user.role|capfirst }}
</p>
</div>
{% empty %}
{% if error %}
<div class=" error">
<h3>Error</h3>
<p>There was a problem connecting to the API.</p>
<code>{{ error }}</code>
<p>
(<em>The above error is shown to the user as an example.
For security reasons these errors are normally hidden from the user</em>)
</p>
</div>
{% else %}
<div>
<p>There are no users available at this time.</p>
</div>
{% endif %}
{% endfor %}
{% endblock %}
{% block footer %}
<div class="footer">
<a href="{% url 'index' %}">Home</a>
</div>
{% endblock %}

4
ch14/apic/rails/tests.py Normal file
View File

@ -0,0 +1,4 @@
# apic/rails/tests.py
from django.test import TestCase
# Create your tests here.

32
ch14/apic/rails/urls.py Normal file
View File

@ -0,0 +1,32 @@
# apic/rails/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
path(
"stations", views.StationsView.as_view(), name="stations"
),
path(
"stations/<int:station_id>/departures",
views.DeparturesView.as_view(),
name="departures",
),
path(
"stations/<int:station_id>/arrivals",
views.ArrivalsView.as_view(),
name="arrivals",
),
path("users", views.UsersView.as_view(), name="users"),
path(
"authenticate",
views.AuthenticateView.as_view(),
name="authenticate",
),
path(
"authenticate/result",
views.AuthenticateResultView.as_view(),
name="auth_result",
),
]

169
ch14/apic/rails/views.py Normal file
View File

@ -0,0 +1,169 @@
# apic/rails/views.py
from datetime import datetime
from operator import itemgetter
from urllib.parse import urljoin
import requests
from django.conf import settings
from django.urls import reverse_lazy
from django.views import generic
from requests.exceptions import RequestException
from .forms import AuthenticateForm
class IndexView(generic.TemplateView):
template_name = "rails/index.html"
class StationsView(generic.TemplateView):
template_name = "rails/stations.html"
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
api_url = urljoin(settings.BASE_API_URL, "stations")
try:
response = requests.get(api_url)
response.raise_for_status()
except RequestException as err:
context["error"] = err
else:
context["stations"] = response.json()
return self.render_to_response(context)
class DeparturesView(generic.TemplateView):
template_name = "rails/departures.html"
def get(self, request, station_id, *args, **kwargs):
context = self.get_context_data(**kwargs)
api_url = urljoin(
settings.BASE_API_URL,
f"stations/{station_id}/departures",
)
try:
response = requests.get(api_url)
response.raise_for_status()
except RequestException as err:
context["error"] = err
else:
trains = prepare_trains(response.json(), "departs_at")
context["departures"] = trains
return self.render_to_response(context)
class ArrivalsView(generic.TemplateView):
template_name = "rails/arrivals.html"
def get(self, request, station_id, *args, **kwargs):
context = self.get_context_data(**kwargs)
api_url = urljoin(
settings.BASE_API_URL,
f"stations/{station_id}/arrivals",
)
try:
response = requests.get(api_url)
response.raise_for_status()
except RequestException as err:
context["error"] = err
else:
trains = prepare_trains(response.json(), "arrives_at")
context["arrivals"] = trains
return self.render_to_response(context)
class UsersView(generic.TemplateView):
template_name = "rails/users.html"
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
api_url = urljoin(
settings.BASE_API_URL,
"users",
)
try:
response = requests.get(api_url)
response.raise_for_status()
except RequestException as err:
context["error"] = err
else:
context["users"] = response.json()
return self.render_to_response(context)
class AuthenticateView(generic.FormView):
template_name = "rails/authenticate.html"
success_url = reverse_lazy("auth_result")
form_class = AuthenticateForm
def form_valid(self, form):
data = form.cleaned_data
self.api_authenticate(data["email"], data["password"])
# leave this as final instruction as it will just perform the redir.
return super().form_valid(form)
def api_authenticate(self, email, password):
api_url = urljoin(
settings.BASE_API_URL,
f"users/authenticate",
)
payload = {
"email": email,
"password": password,
}
try:
response = requests.post(api_url, json=payload)
response.raise_for_status()
except RequestException as err:
self.set_session_data("auth_error", str(err))
else:
key = "token" if response.ok else "auth_error"
self.set_session_data(key, response.json())
def set_session_data(self, key, data):
self.request.session[key] = data
class AuthenticateResultView(generic.TemplateView):
template_name = "rails/authenticate.result.html"
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
context["token"] = request.session.pop("token", None)
context["auth_error"] = request.session.pop(
"auth_error", None
)
return self.render_to_response(context)
def prepare_trains(trains: list[dict], key: str):
return list(
map(
parse_datetimes,
sorted(trains, key=itemgetter(key)),
)
)
def parse_datetimes(train: dict):
train["arrives_at"] = datetime.fromisoformat(
train["arrives_at"]
)
train["departs_at"] = datetime.fromisoformat(
train["departs_at"]
)
return train

6
ch14/pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[tool.black]
line-length = 66
[tool.isort]
atomic=true
line_length=66

5
ch14/requirements/dev.in Normal file
View File

@ -0,0 +1,5 @@
jupyterlab
pdbpp
faker
isort
black

256
ch14/requirements/dev.txt Normal file
View File

@ -0,0 +1,256 @@
#
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile dev.in
#
anyio==3.3.0
# via jupyter-server
appdirs==1.4.4
# via black
appnope==0.1.2
# via
# ipykernel
# ipython
argon2-cffi==20.1.0
# via
# jupyter-server
# notebook
attrs==21.2.0
# via jsonschema
babel==2.9.1
# via jupyterlab-server
backcall==0.2.0
# via ipython
black==21.7b0
# via -r dev.in
bleach==4.0.0
# via nbconvert
certifi==2021.5.30
# via requests
cffi==1.14.6
# via argon2-cffi
charset-normalizer==2.0.4
# via requests
click==8.0.1
# via black
debugpy==1.4.1
# via ipykernel
decorator==5.0.9
# via ipython
defusedxml==0.7.1
# via nbconvert
entrypoints==0.3
# via
# jupyter-client
# jupyterlab-server
# nbconvert
faker==8.12.0
# via -r dev.in
fancycompleter==0.9.1
# via pdbpp
idna==3.2
# via
# anyio
# requests
ipykernel==6.2.0
# via notebook
ipython==7.26.0
# via
# ipykernel
# jupyterlab
ipython-genutils==0.2.0
# via
# jupyter-server
# nbformat
# notebook
# traitlets
isort==5.9.3
# via -r dev.in
jedi==0.18.0
# via ipython
jinja2==3.0.1
# via
# jupyter-server
# jupyterlab
# jupyterlab-server
# nbconvert
# notebook
json5==0.9.6
# via jupyterlab-server
jsonschema==3.2.0
# via
# jupyterlab-server
# nbformat
jupyter-client==7.0.1
# via
# ipykernel
# jupyter-server
# nbclient
# notebook
jupyter-core==4.7.1
# via
# jupyter-client
# jupyter-server
# jupyterlab
# nbconvert
# nbformat
# notebook
jupyter-server==1.10.2
# via
# jupyterlab
# jupyterlab-server
# nbclassic
jupyterlab==3.1.7
# via -r dev.in
jupyterlab-pygments==0.1.2
# via nbconvert
jupyterlab-server==2.7.1
# via jupyterlab
markupsafe==2.0.1
# via jinja2
matplotlib-inline==0.1.2
# via
# ipykernel
# ipython
mistune==0.8.4
# via nbconvert
mypy-extensions==0.4.3
# via black
nbclassic==0.3.1
# via jupyterlab
nbclient==0.5.4
# via nbconvert
nbconvert==6.1.0
# via
# jupyter-server
# notebook
nbformat==5.1.3
# via
# jupyter-server
# nbclient
# nbconvert
# notebook
nest-asyncio==1.5.1
# via
# jupyter-client
# nbclient
notebook==6.4.3
# via nbclassic
packaging==21.0
# via
# bleach
# jupyterlab
# jupyterlab-server
pandocfilters==1.4.3
# via nbconvert
parso==0.8.2
# via jedi
pathspec==0.9.0
# via black
pdbpp==0.10.3
# via -r dev.in
pexpect==4.8.0
# via ipython
pickleshare==0.7.5
# via ipython
prometheus-client==0.11.0
# via
# jupyter-server
# notebook
prompt-toolkit==3.0.20
# via ipython
ptyprocess==0.7.0
# via
# pexpect
# terminado
pycparser==2.20
# via cffi
pygments==2.10.0
# via
# ipython
# jupyterlab-pygments
# nbconvert
# pdbpp
pyparsing==2.4.7
# via packaging
pyrepl==0.9.0
# via fancycompleter
pyrsistent==0.18.0
# via jsonschema
python-dateutil==2.8.2
# via
# faker
# jupyter-client
pytz==2021.1
# via babel
pyzmq==22.2.1
# via
# jupyter-client
# jupyter-server
# notebook
regex==2021.8.3
# via black
requests==2.26.0
# via
# jupyterlab-server
# requests-unixsocket
requests-unixsocket==0.2.0
# via jupyter-server
send2trash==1.8.0
# via
# jupyter-server
# notebook
six==1.16.0
# via
# argon2-cffi
# bleach
# jsonschema
# python-dateutil
sniffio==1.2.0
# via anyio
terminado==0.11.1
# via
# jupyter-server
# notebook
testpath==0.5.0
# via nbconvert
text-unidecode==1.3
# via faker
tomli==1.2.1
# via black
tornado==6.1
# via
# ipykernel
# jupyter-client
# jupyter-server
# jupyterlab
# notebook
# terminado
traitlets==5.0.5
# via
# ipykernel
# ipython
# jupyter-client
# jupyter-core
# jupyter-server
# matplotlib-inline
# nbclient
# nbconvert
# nbformat
# notebook
urllib3==1.26.6
# via
# requests
# requests-unixsocket
wcwidth==0.2.5
# via prompt-toolkit
webencodings==0.5.1
# via bleach
websocket-client==1.2.1
# via jupyter-server
wmctrl==0.4
# via pdbpp
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -0,0 +1,8 @@
django
fastapi
pydantic[email]
pyjwt[crypto]
python-dotenv
requests
sqlalchemy
uvicorn

View File

@ -0,0 +1,62 @@
#
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile requirements.in
#
asgiref==3.4.1
# via
# django
# uvicorn
certifi==2021.5.30
# via requests
cffi==1.14.6
# via cryptography
charset-normalizer==2.0.4
# via requests
click==8.0.1
# via uvicorn
cryptography==3.4.7
# via pyjwt
django==3.2.6
# via -r requirements.in
dnspython==2.1.0
# via email-validator
email-validator==1.1.3
# via pydantic
fastapi==0.68.0
# via -r requirements.in
greenlet==1.1.1
# via sqlalchemy
h11==0.12.0
# via uvicorn
idna==3.2
# via
# email-validator
# requests
pycparser==2.20
# via cffi
pydantic[email]==1.8.2
# via
# -r requirements.in
# fastapi
pyjwt[crypto]==2.1.0
# via -r requirements.in
python-dotenv==0.19.0
# via -r requirements.in
pytz==2021.1
# via django
requests==2.26.0
# via -r requirements.in
sqlalchemy==1.4.23
# via -r requirements.in
sqlparse==0.4.1
# via django
starlette==0.14.2
# via fastapi
typing-extensions==3.10.0.0
# via pydantic
urllib3==1.26.6
# via requests
uvicorn==0.15.0
# via -r requirements.in

View File

@ -0,0 +1,257 @@
$ http http://localhost:8000/stations
HTTP/1.1 200 OK
content-length: 702
content-type: application/json
date: Thu, 19 Aug 2021 22:11:10 GMT
server: uvicorn
[
{
"city": "Rome",
"code": "ROM",
"country": "Italy",
"id": 0
},
{
"city": "Paris",
"code": "PAR",
"country": "France",
"id": 1
},
{
"city": "London",
"code": "LDN",
"country": "UK",
"id": 2
},
{
"city": "Kyiv",
"code": "KYV",
"country": "Ukraine",
"id": 3
},
{
"city": "Stockholm",
"code": "STK",
"country": "Sweden",
"id": 4
},
{
"city": "Warsaw",
"code": "WSW",
"country": "Poland",
"id": 5
},
{
"city": "Moskow",
"code": "MSK",
"country": "Russia",
"id": 6
},
{
"city": "Amsterdam",
"code": "AMD",
"country": "Netherlands",
"id": 7
},
{
"city": "Edinburgh",
"code": "EDB",
"country": "Scotland",
"id": 8
},
{
"city": "Budapest",
"code": "BDP",
"country": "Hungary",
"id": 9
},
{
"city": "Bucharest",
"code": "BCR",
"country": "Romania",
"id": 10
},
{
"city": "Sofia",
"code": "SFA",
"country": "Bulgaria",
"id": 11
}
]
---
$ http http://localhost:8000/stations?code=LDN
HTTP/1.1 200 OK
content-length: 54
content-type: application/json
date: Fri, 20 Aug 2021 19:31:08 GMT
server: uvicorn
[
{
"city": "London",
"code": "LDN",
"country": "UK",
"id": 2
}
]
---
$ http http://localhost:8000/stations/3
HTTP/1.1 200 OK
content-length: 55
content-type: application/json
date: Fri, 20 Aug 2021 19:38:36 GMT
server: uvicorn
{
"city": "Kyiv",
"code": "KYV",
"country": "Ukraine",
"id": 3
}
---
$ http http://localhost:8000/stations/kyiv
HTTP/1.1 422 Unprocessable Entity
content-length: 107
content-type: application/json
date: Fri, 20 Aug 2021 19:42:34 GMT
server: uvicorn
{
"detail": [
{
"loc": [
"path",
"station_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
---
$ http http://localhost:8000/stations/100
HTTP/1.1 404 Not Found
content-length: 35
content-type: application/json
date: Fri, 20 Aug 2021 19:48:09 GMT
server: uvicorn
{
"detail": "Station 100 not found."
}
---
$ http POST http://localhost:8000/stations \
code=TMP country=Temporary-Country city=tmp-city
HTTP/1.1 201 Created
content-length: 70
content-type: application/json
date: Fri, 20 Aug 2021 20:20:34 GMT
server: uvicorn
{
"city": "tmp-city",
"code": "TMP",
"country": "Temporary-Country",
"id": 12
}
---
$ http POST http://localhost:8000/stations \
country=Another-Country city=another-city
HTTP/1.1 422 Unprocessable Entity
content-length: 88
content-type: application/json
date: Fri, 20 Aug 2021 20:23:21 GMT
server: uvicorn
{
"detail": [
{
"loc": [
"body",
"code"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
---
$ http PUT http://localhost:8000/stations/12 \
code=SMC country=Some-Country city=Some-city
HTTP/1.1 204 No Content
date: Fri, 20 Aug 2021 21:19:22 GMT
server: uvicorn
---
$ http http://localhost:8000/stations/12
HTTP/1.1 200 OK
content-length: 66
content-type: application/json
date: Fri, 20 Aug 2021 21:20:39 GMT
server: uvicorn
{
"city": "Some-city",
"code": "SMC",
"country": "Some-Country",
"id": 12
}
---
$ http PUT http://localhost:8000/stations/12 code=xxx
HTTP/1.1 204 No Content
date: Fri, 20 Aug 2021 21:23:09 GMT
server: uvicorn
---
$ http http://localhost:8000/stations/12
HTTP/1.1 200 OK
content-length: 66
content-type: application/json
date: Fri, 20 Aug 2021 21:23:47 GMT
server: uvicorn
{
"city": "Some-city",
"code": "xxx",
"country": "Some-Country",
"id": 12
}
---
$ http DELETE http://localhost:8000/stations/12
HTTP/1.1 204 No Content
date: Fri, 20 Aug 2021 21:33:17 GMT
server: uvicorn
---
$ http DELETE http://localhost:8000/stations/12
HTTP/1.1 404 Not Found
Transfer-Encoding: chunked
date: Fri, 20 Aug 2021 21:33:43 GMT
server: uvicorn
---

View File

@ -0,0 +1,12 @@
$ http POST http://localhost:8000/users/authenticate \
email="fabrizio.romano@example.com" password="f4bPassword"
HTTP/1.1 200 OK
content-length: 201
content-type: application/json
date: Fri, 20 Aug 2021 21:51:13 GMT
server: uvicorn
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Mjk0OTYyNzQsImV4cCI6MTYyOTU4MjY3NCwiZW1haWwiOiJmYWJyaXppby5yb21hbm9AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4ifQ.q-8m_xh2LacyxtNGHzMg2cQhgyDpmyvCr75Qb_7snYI"
---

View File

@ -0,0 +1,77 @@
# samples/typing.examples.py
###############################################
# This module is not supposed to be executed! #
###############################################
# not strongly typed language
a = 7
b = "7"
a + b == 14
concatenate(a, b) == "77"
# dynamic typing
a = 7
a * 2 == 14
a = "Hello"
a * 2 == "HelloHello"
# other languages
String greeting = "Hello";
int m = 7;
float pi = 3.141592;
# function annotations
def greet(first_name, last_name, age):
return f"Greeting {first_name} {last_name} of age {age}"
def greet(
first_name: "First name of the person we are greeting",
last_name: "Last name of the person we are greeting",
age: "The person's age"
) -> "Returns the greeting sentence":
return f"Greeting {first_name} {last_name} of age {age}"
def greet(first_name: str, last_name: str, age: int = 18) -> str:
return f"Greeting {first_name} {last_name} of age {age}"
# list
from typing import List
def process_words(words: List[str]):
for word in words:
# do something with word
# dict
from typing import Dict
def process_users(users: Dict[str, int]):
for name, age in users.items():
# do something with name and age
# optional
from typing import Optional
def greet_again(name: Optional[str] = None):
if name is not None:
print(f"Hello {name}!")
else:
print("Hey dude")
# custom
class Cat:
def __init__(self, name: str):
self.name = name
def call_cat(cat: Cat):
return f"{cat.name}! Come here!"