Add OpenAPI definition and Swagger UI
This commit is contained in:
parent
ada7473610
commit
c38187a64e
12
NOTES.md
12
NOTES.md
@ -6,8 +6,9 @@
|
|||||||
- [X] Figure out how to return JSON or html (`render_template`)
|
- [X] Figure out how to return JSON or html (`render_template`)
|
||||||
- [X] Basic testing
|
- [X] Basic testing
|
||||||
- [X] ruff
|
- [X] ruff
|
||||||
- [ ] GitHub Actions for tests and linters
|
- [X] GitHub Actions for tests and linters
|
||||||
- [ ] Swagger API
|
- [ ] Basic List pages for entities
|
||||||
|
- [X] Swagger API
|
||||||
- [ ] Local development tool to clear/seed database
|
- [ ] Local development tool to clear/seed database
|
||||||
...
|
...
|
||||||
- [ ] Authentication (will need to link `user` table to `player`)
|
- [ ] Authentication (will need to link `user` table to `player`)
|
||||||
@ -40,4 +41,9 @@ https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#create-the-tab
|
|||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
|
|
||||||
https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login
|
https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login
|
||||||
|
|
||||||
|
# Things to learn/fix/understand
|
||||||
|
|
||||||
|
* How to use an existing Python class as the spec in a Swagger API definition? (See, for instance, `responses.200.content.application/json.schema.id` under `get_player`). Perhaps it's not possible just by name, which would be understandable (the context of a Docstring probably doesn't include the context of imported code) - but, in that case, at least I'd like to be able to specify `definitions`` in a way that is independent of a method.
|
||||||
|
* How to specify the content-type that should be sent with a Swagger request without needing to use `requestBody` (and thus, implying that there _should_ be a body)?
|
@ -3,12 +3,15 @@ import sys
|
|||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flasgger import Swagger
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config["SWAGGER"] = {"openapi": "3.0.2"}
|
||||||
|
swagger = Swagger(app)
|
||||||
|
|
||||||
secret_key = os.environ.get("SECRET_KEY")
|
secret_key = os.environ.get("SECRET_KEY")
|
||||||
if not secret_key:
|
if not secret_key:
|
||||||
|
194
app/main.py
194
app/main.py
@ -7,20 +7,93 @@ main = Blueprint("main", __name__)
|
|||||||
|
|
||||||
@main.route("/")
|
@main.route("/")
|
||||||
def index():
|
def index():
|
||||||
|
"""Main Page
|
||||||
|
---
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: A friendly greeting
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"""
|
||||||
return "Hello, World - but new!"
|
return "Hello, World - but new!"
|
||||||
|
|
||||||
|
|
||||||
@main.route("/player", methods=["POST"])
|
@main.route("/player", methods=["POST"])
|
||||||
def create_player():
|
def create_player():
|
||||||
|
"""Create a Player
|
||||||
|
---
|
||||||
|
requestBody:
|
||||||
|
description: Payload describing the player
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: Jim Bloggs
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: Payload containing Player Id
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
tags:
|
||||||
|
- player
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
player = Player(name=data["name"])
|
player = Player(name=data["name"])
|
||||||
db.session.add(player)
|
db.session.add(player)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return {"id": player.id}
|
return {"id": player.id}, 201
|
||||||
|
|
||||||
|
|
||||||
@main.route("/player/<player_id>")
|
@main.route("/player/<player_id>")
|
||||||
def get_player(player_id: str):
|
def get_player(player_id: str):
|
||||||
|
"""Get a Player
|
||||||
|
---
|
||||||
|
parameters:
|
||||||
|
- name: player_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
description: The Player Id
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
text/html:
|
||||||
|
schema: {}
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Payload describing player
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
id: Player
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
404:
|
||||||
|
description: Player not found
|
||||||
|
content:
|
||||||
|
application/json: {}
|
||||||
|
text/html: {}
|
||||||
|
tags:
|
||||||
|
- player
|
||||||
|
"""
|
||||||
|
# TODO - actually, the schema above doesn't reference the `Player` class as I'd hoped it would.
|
||||||
|
# The docs at https://github.com/flasgger/flasgger#extracting-definitions are not super-clear.
|
||||||
|
# _Maybe_ what I'm trying to do is not possible?
|
||||||
player_from_db = db.session.get(Player, int(player_id))
|
player_from_db = db.session.get(Player, int(player_id))
|
||||||
if not player_from_db:
|
if not player_from_db:
|
||||||
return "Not Found", 404
|
return "Not Found", 404
|
||||||
@ -36,6 +109,29 @@ def get_player(player_id: str):
|
|||||||
|
|
||||||
@main.route("/player/<player_id>", methods=["DELETE"])
|
@main.route("/player/<player_id>", methods=["DELETE"])
|
||||||
def delete_player(player_id: str):
|
def delete_player(player_id: str):
|
||||||
|
"""Delete a Player
|
||||||
|
---
|
||||||
|
parameters:
|
||||||
|
- name: player_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
description: The Player Id
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: Empty
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
tags:
|
||||||
|
- player
|
||||||
|
"""
|
||||||
# Note - no checking that the player exists, because HTTP semantics specify
|
# Note - no checking that the player exists, because HTTP semantics specify
|
||||||
# that `DELETE` should be idempotent.
|
# that `DELETE` should be idempotent.
|
||||||
db.session.query(Player).filter(Player.id == int(player_id)).delete()
|
db.session.query(Player).filter(Player.id == int(player_id)).delete()
|
||||||
@ -45,6 +141,43 @@ def delete_player(player_id: str):
|
|||||||
|
|
||||||
@main.route("/deck", methods=["POST"])
|
@main.route("/deck", methods=["POST"])
|
||||||
def create_deck():
|
def create_deck():
|
||||||
|
"""Create a Deck
|
||||||
|
---
|
||||||
|
requestBody:
|
||||||
|
description: Payload describing the deck
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: My First Deck
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
example: Better than yours!
|
||||||
|
owner_id:
|
||||||
|
type: number
|
||||||
|
example: 1
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- owner_id
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: Payload containing Deck Id
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
400:
|
||||||
|
description: Owner not found
|
||||||
|
tags:
|
||||||
|
- deck
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
owner_id = data["owner_id"]
|
owner_id = data["owner_id"]
|
||||||
|
|
||||||
@ -58,11 +191,45 @@ def create_deck():
|
|||||||
db.session.add(deck)
|
db.session.add(deck)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Finished creating the deck!")
|
print("Finished creating the deck!")
|
||||||
return {"id": deck.id}
|
return {"id": deck.id}, 201
|
||||||
|
|
||||||
|
|
||||||
@main.route("/deck/<deck_id>")
|
@main.route("/deck/<deck_id>")
|
||||||
def get_deck(deck_id: str):
|
def get_deck(deck_id: str):
|
||||||
|
"""Get a Deck
|
||||||
|
---
|
||||||
|
parameters:
|
||||||
|
- name: deck_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
description: The Deck Id
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
text/html:
|
||||||
|
schema: {}
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Payload describing deck
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
id: Deck
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
404:
|
||||||
|
description: Deck not found
|
||||||
|
content:
|
||||||
|
application/json: {}
|
||||||
|
text/html: {}
|
||||||
|
tags:
|
||||||
|
- deck
|
||||||
|
"""
|
||||||
deck_from_db = db.session.get(Deck, int(deck_id))
|
deck_from_db = db.session.get(Deck, int(deck_id))
|
||||||
if not deck_from_db:
|
if not deck_from_db:
|
||||||
return "Not Found", 404
|
return "Not Found", 404
|
||||||
@ -81,6 +248,29 @@ def get_deck(deck_id: str):
|
|||||||
|
|
||||||
@main.route("/deck/<deck_id>", methods=["DELETE"])
|
@main.route("/deck/<deck_id>", methods=["DELETE"])
|
||||||
def delete_deck(deck_id: str):
|
def delete_deck(deck_id: str):
|
||||||
|
"""Delete a Deck
|
||||||
|
---
|
||||||
|
parameters:
|
||||||
|
- name: deck_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
description: The Deck Id
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: Empty
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
tags:
|
||||||
|
- deck
|
||||||
|
"""
|
||||||
db.session.query(Deck).filter(Deck.id == int(deck_id)).delete()
|
db.session.query(Deck).filter(Deck.id == int(deck_id)).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return "", 204
|
return "", 204
|
||||||
|
@ -12,7 +12,9 @@ def test_add_and_retrieve_player(client: FlaskClient):
|
|||||||
response = _json_get(client, "/player/1")
|
response = _json_get(client, "/player/1")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
_json_post(client, "/player", {"name": "jason"})
|
create_player_response = _json_post(client, "/player", {"name": "jason"})
|
||||||
|
assert create_player_response.status_code == 201
|
||||||
|
|
||||||
response_1 = _json_get(client, "/player/1")
|
response_1 = _json_get(client, "/player/1")
|
||||||
assert response_1.json["name"] == "jason"
|
assert response_1.json["name"] == "jason"
|
||||||
|
|
||||||
@ -34,13 +36,13 @@ def test_add_and_retrieve_deck(client: FlaskClient):
|
|||||||
assert invalid_owner_response.text == "Owner id 1 not found"
|
assert invalid_owner_response.text == "Owner id 1 not found"
|
||||||
|
|
||||||
create_jim_response = _json_post(client, "/player", {"name": "jim"})
|
create_jim_response = _json_post(client, "/player", {"name": "jim"})
|
||||||
assert create_jim_response.status_code == 200
|
assert create_jim_response.status_code == 201
|
||||||
jim_id = create_jim_response.json["id"]
|
jim_id = create_jim_response.json["id"]
|
||||||
|
|
||||||
create_deck_response = _json_post(
|
create_deck_response = _json_post(
|
||||||
client, "/deck", {"name": "Baby's First Deck", "owner_id": str(jim_id)}
|
client, "/deck", {"name": "Baby's First Deck", "owner_id": str(jim_id)}
|
||||||
)
|
)
|
||||||
assert create_deck_response.status_code == 200
|
assert create_deck_response.status_code == 201
|
||||||
# _Should_ always be 1, since we expect to start with an empty database, but why risk it?
|
# _Should_ always be 1, since we expect to start with an empty database, but why risk it?
|
||||||
deck_id = create_deck_response.json["id"]
|
deck_id = create_deck_response.json["id"]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user