ch14
This commit is contained in:
parent
03bb4d43c0
commit
0a0ceaf7d7
56
ch14/README.md
Normal file
56
ch14/README.md
Normal 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
4
ch14/api_code/.env
Normal file
@ -0,0 +1,4 @@
|
||||
# api_code/.env
|
||||
SECRET_KEY="ec604d5610ac4668a44418711be8251f"
|
||||
DEBUG=false
|
||||
API_VERSION=1.0.0
|
||||
1
ch14/api_code/api/__init__.py
Normal file
1
ch14/api_code/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# api_code/api/__init__.py
|
||||
42
ch14/api_code/api/admin.py
Normal file
42
ch14/api_code/api/admin.py
Normal 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
ch14/api_code/api/config.py
Normal file
11
ch14/api_code/api/config.py
Normal 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
231
ch14/api_code/api/crud.py
Normal 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
ch14/api_code/api/database.py
Normal file
24
ch14/api_code/api/database.py
Normal 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
20
ch14/api_code/api/deps.py
Normal 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
181
ch14/api_code/api/models.py
Normal 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
ch14/api_code/api/schemas.py
Normal file
118
ch14/api_code/api/schemas.py
Normal 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
ch14/api_code/api/stations.py
Normal file
122
ch14/api_code/api/stations.py
Normal 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
ch14/api_code/api/tickets.py
Normal file
53
ch14/api_code/api/tickets.py
Normal 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
ch14/api_code/api/trains.py
Normal file
84
ch14/api_code/api/trains.py
Normal 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
146
ch14/api_code/api/users.py
Normal 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
54
ch14/api_code/api/util.py
Normal 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
182
ch14/api_code/dummy_data.py
Normal 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
20
ch14/api_code/main.py
Normal 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
125
ch14/api_code/queries.md
Normal 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
BIN
ch14/api_code/train.db
Normal file
Binary file not shown.
1
ch14/apic/apic/__init__.py
Normal file
1
ch14/apic/apic/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# apic/apic/__init__.py
|
||||
17
ch14/apic/apic/asgi.py
Normal file
17
ch14/apic/apic/asgi.py
Normal 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
132
ch14/apic/apic/settings.py
Normal 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
23
ch14/apic/apic/urls.py
Normal 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
17
ch14/apic/apic/wsgi.py
Normal 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
27
ch14/apic/manage.py
Normal 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()
|
||||
1
ch14/apic/rails/__init__.py
Normal file
1
ch14/apic/rails/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# apic/rails/__init__.py
|
||||
4
ch14/apic/rails/admin.py
Normal file
4
ch14/apic/rails/admin.py
Normal 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
7
ch14/apic/rails/apps.py
Normal 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
9
ch14/apic/rails/forms.py
Normal 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
|
||||
)
|
||||
1
ch14/apic/rails/migrations/__init__.py
Normal file
1
ch14/apic/rails/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# apic/rails/migrations/__init__.py
|
||||
4
ch14/apic/rails/models.py
Normal file
4
ch14/apic/rails/models.py
Normal file
@ -0,0 +1,4 @@
|
||||
# apic/rails/models.py
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
29
ch14/apic/rails/static/rails/style.css
Normal file
29
ch14/apic/rails/static/rails/style.css
Normal 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;
|
||||
}
|
||||
54
ch14/apic/rails/templates/rails/arrivals.html
Normal file
54
ch14/apic/rails/templates/rails/arrivals.html
Normal 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 %}
|
||||
21
ch14/apic/rails/templates/rails/authenticate.html
Normal file
21
ch14/apic/rails/templates/rails/authenticate.html
Normal 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 %}
|
||||
38
ch14/apic/rails/templates/rails/authenticate.result.html
Normal file
38
ch14/apic/rails/templates/rails/authenticate.result.html
Normal 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 %}
|
||||
17
ch14/apic/rails/templates/rails/base.html
Normal file
17
ch14/apic/rails/templates/rails/base.html
Normal 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>
|
||||
53
ch14/apic/rails/templates/rails/departures.html
Normal file
53
ch14/apic/rails/templates/rails/departures.html
Normal 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 %}
|
||||
26
ch14/apic/rails/templates/rails/index.html
Normal file
26
ch14/apic/rails/templates/rails/index.html
Normal 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 %}
|
||||
52
ch14/apic/rails/templates/rails/stations.html
Normal file
52
ch14/apic/rails/templates/rails/stations.html
Normal 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 }} <Code: {{ station.code }}
|
||||
({{ station.city }}, {{ station.country }})>
|
||||
<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 %}
|
||||
50
ch14/apic/rails/templates/rails/users.html
Normal file
50
ch14/apic/rails/templates/rails/users.html
Normal 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 }} <{{ user.full_name }}
|
||||
(<a href="mailto: {{ user.email }})">{{ user.email }}</a>>
|
||||
{{ 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
4
ch14/apic/rails/tests.py
Normal 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
32
ch14/apic/rails/urls.py
Normal 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
169
ch14/apic/rails/views.py
Normal 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
6
ch14/pyproject.toml
Normal file
@ -0,0 +1,6 @@
|
||||
[tool.black]
|
||||
line-length = 66
|
||||
|
||||
[tool.isort]
|
||||
atomic=true
|
||||
line_length=66
|
||||
5
ch14/requirements/dev.in
Normal file
5
ch14/requirements/dev.in
Normal file
@ -0,0 +1,5 @@
|
||||
jupyterlab
|
||||
pdbpp
|
||||
faker
|
||||
isort
|
||||
black
|
||||
256
ch14/requirements/dev.txt
Normal file
256
ch14/requirements/dev.txt
Normal 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
|
||||
8
ch14/requirements/requirements.in
Normal file
8
ch14/requirements/requirements.in
Normal file
@ -0,0 +1,8 @@
|
||||
django
|
||||
fastapi
|
||||
pydantic[email]
|
||||
pyjwt[crypto]
|
||||
python-dotenv
|
||||
requests
|
||||
sqlalchemy
|
||||
uvicorn
|
||||
62
ch14/requirements/requirements.txt
Normal file
62
ch14/requirements/requirements.txt
Normal 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
|
||||
257
ch14/samples/api.calls/stations.txt
Normal file
257
ch14/samples/api.calls/stations.txt
Normal 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
|
||||
|
||||
---
|
||||
12
ch14/samples/api.calls/users.txt
Normal file
12
ch14/samples/api.calls/users.txt
Normal 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"
|
||||
|
||||
---
|
||||
|
||||
77
ch14/samples/typing.examples.py
Normal file
77
ch14/samples/typing.examples.py
Normal 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!"
|
||||
Loading…
x
Reference in New Issue
Block a user