Skip to content

Commit 0a0ceaf

Browse files
committed
ch14
1 parent 03bb4d4 commit 0a0ceaf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2945
-0
lines changed

ch14/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# CH14 - Readme
2+
3+
This chapter is about APIs. You will find three main folders:
4+
5+
1. `api_code` contains the FastAPI project about Railways
6+
2. `apic` or API Consumer. This is a Django project to show an example on how to talk to an API from another application.
7+
3. `samples`. This folder contains the examples that went in the chapter, so you can ignore it.
8+
9+
## Setup
10+
11+
Install requirements with pip from their folder:
12+
13+
$ pip install -r requirements.txt
14+
15+
If you want to create your own dummy data, please also install the dev requirements:
16+
17+
$ pip install -r dev.txt
18+
19+
To generate a new database with random data:
20+
21+
$ cd api_code
22+
$ python dummy_data.py
23+
24+
This will generate a new `train.db` file for you.
25+
26+
## Running the API
27+
28+
Open a terminal window, change into the `api_code` folder and type this command:
29+
30+
$ uvicorn main:app --reload
31+
32+
The `--reload` flag makes sure uvicorn is reloaded if you make changes to the
33+
source while the app is running.
34+
35+
## Running the Django consumer
36+
37+
While the API is running in a terminal, open another terminal window,
38+
change into the `apic` folder, and type the following:
39+
40+
$ python manage.py migrate # only the first time, to generate the django db
41+
42+
Once migrations have been applied, run this to start the app:
43+
44+
$ python manage.py runserver 8080
45+
46+
We need to specify a port for Django that is not 8000 (its normal default),
47+
since the API is running on that one.
48+
49+
When the Django app is running, open a browser and go to: http://localhost:8080
50+
51+
You should see the welcome page of the Railways app. From there you can navigate using links.
52+
53+
In the authentication section, you can use these credentials to authenticate:
54+
55+
56+
password: f4bPassword

ch14/api_code/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# api_code/.env
2+
SECRET_KEY="ec604d5610ac4668a44418711be8251f"
3+
DEBUG=false
4+
API_VERSION=1.0.0

ch14/api_code/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# api_code/api/__init__.py

ch14/api_code/api/admin.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# api_code/api/admin.py
2+
from typing import Optional
3+
4+
from fastapi import (
5+
APIRouter,
6+
Depends,
7+
Header,
8+
HTTPException,
9+
Response,
10+
status,
11+
)
12+
from sqlalchemy.orm import Session
13+
14+
from . import crud
15+
from .deps import Settings, get_db, get_settings
16+
from .util import is_admin
17+
18+
router = APIRouter(prefix="/admin")
19+
20+
21+
def ensure_admin(settings: Settings, authorization: str):
22+
if not is_admin(
23+
settings=settings, authorization=authorization
24+
):
25+
raise HTTPException(
26+
status_code=status.HTTP_401_UNAUTHORIZED,
27+
detail=f"You must be an admin to access this endpoint.",
28+
)
29+
30+
31+
@router.delete("/stations/{station_id}", tags=["Admin"])
32+
def admin_delete_station(
33+
station_id: int,
34+
authorization: Optional[str] = Header(None),
35+
settings: Settings = Depends(get_settings),
36+
db: Session = Depends(get_db),
37+
):
38+
ensure_admin(settings, authorization)
39+
row_count = crud.delete_station(db=db, station_id=station_id)
40+
if row_count:
41+
return Response(status_code=status.HTTP_204_NO_CONTENT)
42+
return Response(status_code=status.HTTP_404_NOT_FOUND)

ch14/api_code/api/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# api_code/api/config.py
2+
from pydantic import BaseSettings
3+
4+
5+
class Settings(BaseSettings):
6+
secret_key: str
7+
debug: bool
8+
api_version: str
9+
10+
class Config:
11+
env_file = ".env"

ch14/api_code/api/crud.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# api_code/api/crud.py
2+
from datetime import datetime, timezone
3+
4+
from sqlalchemy import delete, update
5+
from sqlalchemy.orm import Session, aliased
6+
7+
from . import models, schemas
8+
9+
# USERS
10+
11+
12+
def get_users(db: Session, email: str = None):
13+
q = db.query(models.User)
14+
if email is not None:
15+
q = q.filter(models.User.email.ilike(f"%{email}%"))
16+
return q.all()
17+
18+
19+
def get_user(db: Session, user_id: int):
20+
return (
21+
db.query(models.User)
22+
.filter(models.User.id == user_id)
23+
.first()
24+
)
25+
26+
27+
def get_user_by_email(db: Session, email: str):
28+
return (
29+
db.query(models.User)
30+
.filter(models.User.email.ilike(email))
31+
.first()
32+
)
33+
34+
35+
def create_user(
36+
db: Session, user: schemas.UserCreate, user_id: int = None
37+
):
38+
hashed_password = models.User.hash_password(user.password)
39+
user_dict = {
40+
**user.dict(exclude_unset=True),
41+
"password": hashed_password,
42+
}
43+
if user_id is not None:
44+
user_dict.update({"id": user_id})
45+
db_user = models.User(**user_dict)
46+
db.add(db_user)
47+
db.commit()
48+
db.refresh(db_user)
49+
return db_user
50+
51+
52+
def update_user(
53+
db: Session, user: schemas.UserUpdate, user_id: int
54+
):
55+
user_dict = {
56+
**user.dict(exclude_unset=True),
57+
}
58+
if user.password is not None:
59+
user_dict.update(
60+
{"password": models.User.hash_password(user.password)}
61+
)
62+
stm = (
63+
update(models.User)
64+
.where(models.User.id == user_id)
65+
.values(user_dict)
66+
)
67+
result = db.execute(stm)
68+
db.commit()
69+
return result.rowcount
70+
71+
72+
def delete_user(db: Session, user_id: int):
73+
stm = delete(models.User).where(models.User.id == user_id)
74+
result = db.execute(stm)
75+
db.commit()
76+
return result.rowcount
77+
78+
79+
# STATIONS
80+
81+
82+
def get_stations(db: Session, code: str = None):
83+
q = db.query(models.Station)
84+
if code is not None:
85+
q = q.filter(models.Station.code.ilike(code))
86+
return q.all()
87+
88+
89+
def get_station(db: Session, station_id: int):
90+
return (
91+
db.query(models.Station)
92+
.filter(models.Station.id == station_id)
93+
.first()
94+
)
95+
96+
97+
def get_station_by_code(db: Session, code: str):
98+
return (
99+
db.query(models.Station)
100+
.filter(models.Station.code.ilike(code))
101+
.first()
102+
)
103+
104+
105+
def create_station(
106+
db: Session,
107+
station: schemas.StationCreate,
108+
):
109+
db_station = models.Station(**station.dict())
110+
db.add(db_station)
111+
db.commit()
112+
db.refresh(db_station)
113+
return db_station
114+
115+
116+
def update_station(
117+
db: Session, station: schemas.StationUpdate, station_id: int
118+
):
119+
stm = (
120+
update(models.Station)
121+
.where(models.Station.id == station_id)
122+
.values(station.dict(exclude_unset=True))
123+
)
124+
result = db.execute(stm)
125+
db.commit()
126+
return result.rowcount
127+
128+
129+
def delete_station(db: Session, station_id: int):
130+
stm = delete(models.Station).where(
131+
models.Station.id == station_id
132+
)
133+
result = db.execute(stm)
134+
db.commit()
135+
return result.rowcount
136+
137+
138+
# TRAINS
139+
140+
141+
def get_trains(
142+
db: Session,
143+
station_from_code: str = None,
144+
station_to_code: str = None,
145+
include_all: bool = False,
146+
):
147+
q = db.query(models.Train)
148+
149+
if station_from_code is not None:
150+
st_from = aliased(models.Station)
151+
q = q.join(st_from, models.Train.station_from)
152+
q = q.filter(st_from.code.ilike(f"%{station_from_code}%"))
153+
154+
if station_to_code is not None:
155+
st_to = aliased(models.Station)
156+
q = q.join(st_to, models.Train.station_to)
157+
q = q.filter(st_to.code.ilike(f"%{station_to_code}%"))
158+
159+
if not include_all:
160+
now = datetime.now(tz=timezone.utc)
161+
q = q.filter(models.Train.departs_at > now)
162+
163+
return q.all()
164+
165+
166+
def get_train(db: Session, train_id: int):
167+
return (
168+
db.query(models.Train)
169+
.filter(models.Train.id == train_id)
170+
.first()
171+
)
172+
173+
174+
def get_train_by_name(db: Session, name: str):
175+
return (
176+
db.query(models.Train)
177+
.filter(models.Train.name == name)
178+
.first()
179+
)
180+
181+
182+
def create_train(db: Session, train: schemas.TrainCreate):
183+
train_dict = train.dict(exclude_unset=True)
184+
db_train = models.Train(**train_dict)
185+
db.add(db_train)
186+
db.commit()
187+
db.refresh(db_train)
188+
return db_train
189+
190+
191+
def delete_train(db: Session, train_id: int):
192+
stm = delete(models.Train).where(models.Train.id == train_id)
193+
result = db.execute(stm)
194+
db.commit()
195+
return result.rowcount
196+
197+
198+
# TICKETS
199+
200+
201+
def get_tickets(db: Session):
202+
return db.query(models.Ticket).all()
203+
204+
205+
def get_ticket(db: Session, ticket_id: int):
206+
return (
207+
db.query(models.Ticket)
208+
.filter(models.Ticket.id == ticket_id)
209+
.first()
210+
)
211+
212+
213+
def create_ticket(db: Session, ticket: schemas.TicketCreate):
214+
ticket_dict = ticket.dict(exclude_unset=True)
215+
ticket_dict.update(
216+
{"created_at": datetime.now(tz=timezone.utc)}
217+
)
218+
db_ticket = models.Ticket(**ticket_dict)
219+
db.add(db_ticket)
220+
db.commit()
221+
db.refresh(db_ticket)
222+
return db_ticket
223+
224+
225+
def delete_ticket(db: Session, ticket_id: int):
226+
stm = delete(models.Ticket).where(
227+
models.Ticket.id == ticket_id
228+
)
229+
result = db.execute(stm)
230+
db.commit()
231+
return result.rowcount

ch14/api_code/api/database.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# api_code/api/database.py
2+
from sqlalchemy import create_engine
3+
from sqlalchemy.ext.declarative import declarative_base
4+
from sqlalchemy.orm import sessionmaker
5+
6+
from .config import Settings
7+
8+
settings = Settings()
9+
10+
11+
DB_URL = "sqlite:///train.db"
12+
13+
14+
engine = create_engine(
15+
DB_URL,
16+
connect_args={"check_same_thread": False},
17+
echo=settings.debug, # when debug is True, queries are logged
18+
)
19+
20+
SessionLocal = sessionmaker(
21+
autocommit=False, autoflush=False, bind=engine
22+
)
23+
24+
Base = declarative_base()

ch14/api_code/api/deps.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# api_code/api/deps.py
2+
from functools import lru_cache
3+
4+
from .config import Settings
5+
from .database import SessionLocal
6+
7+
8+
def get_db():
9+
"""Return a DB Session."""
10+
db = SessionLocal()
11+
try:
12+
yield db
13+
finally:
14+
db.close()
15+
16+
17+
@lru_cache
18+
def get_settings():
19+
"""Return the app settings."""
20+
return Settings()

0 commit comments

Comments
 (0)