Skip to content

Commit bd7bc5e

Browse files
authored
add tenant api (#24)
2 parents 86a1e38 + 7cf0526 commit bd7bc5e

File tree

8 files changed

+338
-126
lines changed

8 files changed

+338
-126
lines changed

pyproject.toml

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ dependencies = [
1616
"psycopg<4.0.0,>=3.1.19",
1717
"alembic<2.0.0,>=1.13.1",
1818
"pydantic<3.0.0,>=2.7.1",
19-
"flask-restful<1.0.0,>=0.3.10",
2019
]
2120

2221
[project.scripts]

tests/test_tenant.py

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from flask.testing import FlaskClient
2+
from sqlalchemy.sql import func, select
3+
4+
from teufa import db as dbm
5+
from teufa.ext import db
6+
7+
8+
def test_create_tenant(client: FlaskClient):
9+
response = client.post(
10+
"/api/v1/admin/tenants",
11+
json={
12+
"tenant": {
13+
"name": "Test Tenant",
14+
"hostname": "test.tenant.com",
15+
}
16+
},
17+
)
18+
19+
assert response.status_code == 201
20+
assert response.json == {
21+
"tenant": {
22+
"id": 1,
23+
"name": "Test Tenant",
24+
"hostname": "test.tenant.com",
25+
}
26+
}
27+
28+
29+
def test_get_tenant(client: FlaskClient):
30+
tenant = dbm.Tenant(
31+
name="Test Tenant",
32+
hostname="test.tenant.com",
33+
)
34+
db.session.add(tenant)
35+
db.session.commit()
36+
37+
response = client.get(f"/api/v1/admin/tenants/{tenant.id}")
38+
39+
assert response.status_code == 200
40+
assert response.json == {
41+
"tenant": {
42+
"id": tenant.id,
43+
"name": "Test Tenant",
44+
"hostname": "test.tenant.com",
45+
}
46+
}
47+
48+
49+
def test_get_tenant_not_found(client: FlaskClient):
50+
response = client.get("/api/v1/admin/tenants/1")
51+
52+
assert response.status_code == 404
53+
assert response.json == {"message": "Tenant not found"}
54+
55+
56+
def test_update_tenant(client: FlaskClient):
57+
tenant = dbm.Tenant(
58+
name="Test Tenant",
59+
hostname="test.tenant.com",
60+
)
61+
db.session.add(tenant)
62+
db.session.commit()
63+
64+
response = client.put(
65+
f"/api/v1/admin/tenants/{tenant.id}",
66+
json={
67+
"tenant": {
68+
"name": "Updated Tenant",
69+
"hostname": "updated.tenant.com",
70+
}
71+
},
72+
)
73+
74+
assert response.status_code == 200
75+
assert response.json == {
76+
"tenant": {
77+
"id": tenant.id,
78+
"name": "Updated Tenant",
79+
"hostname": "updated.tenant.com",
80+
}
81+
}
82+
83+
84+
def test_update_tenant_not_found(client: FlaskClient):
85+
response = client.put(
86+
"/api/v1/admin/tenants/1",
87+
json={
88+
"tenant": {
89+
"name": "Updated Tenant",
90+
"hostname": "updated.tenant.com",
91+
}
92+
},
93+
)
94+
95+
assert response.status_code == 404
96+
assert response.json == {"message": "Tenant not found"}
97+
98+
99+
def test_delete_tenant(client: FlaskClient):
100+
tenant = dbm.Tenant(
101+
name="Test Tenant",
102+
hostname="test.tenant.com",
103+
)
104+
db.session.add(tenant)
105+
db.session.commit()
106+
107+
response = client.delete(f"/api/v1/admin/tenants/{tenant.id}")
108+
109+
assert response.status_code == 204
110+
assert response.data == b""
111+
112+
with db.session.begin():
113+
assert db.session.scalar(select(func.count(dbm.Tenant.id))) == 0
114+
115+
116+
def test_delete_tenant_not_found(client: FlaskClient):
117+
response = client.delete("/api/v1/admin/tenants/1")
118+
119+
assert response.status_code == 404
120+
assert response.json == {"message": "Tenant not found"}

teufa/app.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .config import Config
44
from .ext import db
5-
from .v1_api import bp as v1_bp
5+
from .v1_api import v1_bp
66

77

88
def create_app():

teufa/dao.py

+32
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,38 @@ class Error(BaseModel):
77
message: str
88

99

10+
class Tenant(BaseModel):
11+
id: int | None = None
12+
name: str
13+
hostname: str
14+
15+
16+
class PartialTenant(BaseModel):
17+
id: int | object = empty
18+
name: str | object = empty
19+
hostname: str | object = empty
20+
21+
22+
class CreateTenantRequest(BaseModel):
23+
tenant: Tenant
24+
25+
26+
class CreateTenantResponse(BaseModel):
27+
tenant: Tenant
28+
29+
30+
class UpdateTenantRequest(BaseModel):
31+
tenant: PartialTenant
32+
33+
34+
class UpdateTenantResponse(BaseModel):
35+
tenant: Tenant
36+
37+
38+
class GetTenantResponse(BaseModel):
39+
tenant: Tenant
40+
41+
1042
class Flight(BaseModel):
1143
id: int | None = None
1244
departure_icao: str

teufa/v1_api/__init__.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
from flask import Blueprint, Flask, g, request
2-
from flask_restful import Api
1+
from flask import Blueprint, g, request
32
from sqlalchemy import select
43

54
from .. import db as dbm
65
from ..ext import db
7-
from .flights import FlightCollectionResource, FlightResource
6+
from .flights import flights_bp
7+
from .tenants import tenants_bp
88

9-
bp = Blueprint("api", __name__, url_prefix="/api")
9+
v1_bp = Blueprint("v1", __name__, url_prefix="/api/v1")
1010

11+
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
1112

12-
@bp.before_request
13+
admin_bp.register_blueprint(tenants_bp)
14+
v1_bp.register_blueprint(admin_bp)
15+
16+
api_bp = Blueprint("api", __name__, url_prefix="/")
17+
18+
19+
@api_bp.before_request
1320
def before_request():
1421
if not hasattr(g, "tenant"):
1522
hostname = request.host.split(":")[0]
@@ -18,7 +25,5 @@ def before_request():
1825
).first()
1926

2027

21-
api = Api(bp)
22-
23-
api.add_resource(FlightCollectionResource, "/v1/flights")
24-
api.add_resource(FlightResource, "/v1/flights/<int:flight_id>")
28+
api_bp.register_blueprint(flights_bp)
29+
v1_bp.register_blueprint(api_bp)

teufa/v1_api/flights.py

+72-71
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,103 @@
1-
from flask import g, request
2-
from flask_restful import Resource
1+
from flask import Blueprint, g, jsonify, request
32

43
from .. import dao
54
from .. import db as dbm
65
from ..ext import db
76

7+
flights_bp = Blueprint("flights", __name__)
88

9-
class FlightCollectionResource(Resource):
10-
def post(self):
11-
data = request.get_json()
129

13-
req = dao.CreateFlightRequest(**data)
10+
@flights_bp.route("/flights", methods=["POST"])
11+
def create_flight():
12+
data = request.get_json()
13+
req = dao.CreateFlightRequest(**data)
1414

15-
flight = dbm.Flight(
16-
tenant_id=g.tenant.id,
17-
departure_icao=req.flight.departure_icao,
18-
arrival_icao=req.flight.arrival_icao,
19-
aircraft_id=req.flight.aircraft_id,
20-
)
15+
flight = dbm.Flight(
16+
tenant_id=g.tenant.id,
17+
departure_icao=req.flight.departure_icao,
18+
arrival_icao=req.flight.arrival_icao,
19+
aircraft_id=req.flight.aircraft_id,
20+
)
2121

22-
db.session.add(flight)
23-
db.session.commit()
22+
db.session.add(flight)
23+
db.session.commit()
2424

25-
res = dao.CreateFlightResponse(
26-
**{
27-
"flight": {
28-
"id": flight.id,
29-
"departure_icao": flight.departure_icao,
30-
"arrival_icao": flight.arrival_icao,
31-
"aircraft_id": flight.aircraft_id,
32-
}
25+
res = dao.CreateFlightResponse(
26+
**{
27+
"flight": {
28+
"id": flight.id,
29+
"departure_icao": flight.departure_icao,
30+
"arrival_icao": flight.arrival_icao,
31+
"aircraft_id": flight.aircraft_id,
3332
}
34-
)
33+
}
34+
)
3535

36-
return res.model_dump(), 201
36+
return jsonify(res.model_dump()), 201
3737

3838

39-
class FlightResource(Resource):
40-
def get(self, flight_id):
41-
flight = db.session.get(dbm.Flight, flight_id)
39+
@flights_bp.route("/flights/<int:flight_id>", methods=["GET"])
40+
def get_flight(flight_id):
41+
flight = db.session.get(dbm.Flight, flight_id)
4242

43-
if not flight:
44-
return dao.Error(message="Flight not found").model_dump(), 404
43+
if not flight:
44+
return jsonify(dao.Error(message="Flight not found").model_dump()), 404
4545

46-
res = dao.GetFlightResponse(
47-
**{
48-
"flight": {
49-
"id": flight.id,
50-
"departure_icao": flight.departure_icao,
51-
"arrival_icao": flight.arrival_icao,
52-
"aircraft_id": flight.aircraft_id,
53-
}
46+
res = dao.GetFlightResponse(
47+
**{
48+
"flight": {
49+
"id": flight.id,
50+
"departure_icao": flight.departure_icao,
51+
"arrival_icao": flight.arrival_icao,
52+
"aircraft_id": flight.aircraft_id,
5453
}
55-
)
54+
}
55+
)
5656

57-
return res.model_dump()
57+
return jsonify(res.model_dump())
5858

59-
def put(self, flight_id):
60-
flight = db.session.get(dbm.Flight, flight_id)
6159

62-
if not flight:
63-
return dao.Error(message="Flight not found").model_dump(), 404
60+
@flights_bp.route("/flights/<int:flight_id>", methods=["PUT"])
61+
def update_flight(flight_id):
62+
flight = db.session.get(dbm.Flight, flight_id)
6463

65-
data = request.get_json()
64+
if not flight:
65+
return jsonify(dao.Error(message="Flight not found").model_dump()), 404
6666

67-
req = dao.UpdateFlightRequest(**data)
67+
data = request.get_json()
68+
req = dao.UpdateFlightRequest(**data)
6869

69-
if req.flight.id is not dao.empty:
70-
flight.id = req.flight.id
71-
if req.flight.departure_icao is not dao.empty:
72-
flight.departure_icao = req.flight.departure_icao
73-
if req.flight.arrival_icao is not dao.empty:
74-
flight.arrival_icao = req.flight.arrival_icao
75-
if req.flight.aircraft_id is not dao.empty:
76-
flight.aircraft_id = req.flight.aircraft_id
70+
if req.flight.departure_icao is not dao.empty:
71+
flight.departure_icao = req.flight.departure_icao
72+
if req.flight.arrival_icao is not dao.empty:
73+
flight.arrival_icao = req.flight.arrival_icao
74+
if req.flight.aircraft_id is not dao.empty:
75+
flight.aircraft_id = req.flight.aircraft_id
7776

78-
db.session.commit()
77+
db.session.commit()
7978

80-
res = dao.UpdateFlightResponse(
81-
**{
82-
"flight": {
83-
"id": flight.id,
84-
"departure_icao": flight.departure_icao,
85-
"arrival_icao": flight.arrival_icao,
86-
"aircraft_id": flight.aircraft_id,
87-
}
79+
res = dao.UpdateFlightResponse(
80+
**{
81+
"flight": {
82+
"id": flight.id,
83+
"departure_icao": flight.departure_icao,
84+
"arrival_icao": flight.arrival_icao,
85+
"aircraft_id": flight.aircraft_id,
8886
}
89-
)
87+
}
88+
)
9089

91-
return res.model_dump()
90+
return jsonify(res.model_dump())
9291

93-
def delete(self, flight_id):
94-
flight = db.session.get(dbm.Flight, flight_id)
9592

96-
if not flight:
97-
return dao.Error(message="Flight not found").model_dump(), 404
93+
@flights_bp.route("/flights/<int:flight_id>", methods=["DELETE"])
94+
def delete_flight(flight_id):
95+
flight = db.session.get(dbm.Flight, flight_id)
9896

99-
db.session.delete(flight)
100-
db.session.commit()
97+
if not flight:
98+
return jsonify(dao.Error(message="Flight not found").model_dump()), 404
10199

102-
return "", 204
100+
db.session.delete(flight)
101+
db.session.commit()
102+
103+
return "", 204

0 commit comments

Comments
 (0)