Add OpenAPI definition and Swagger UI
This commit is contained in:
parent
ada7473610
commit
c38187a64e
10
NOTES.md
10
NOTES.md
@ -6,8 +6,9 @@
|
||||
- [X] Figure out how to return JSON or html (`render_template`)
|
||||
- [X] Basic testing
|
||||
- [X] ruff
|
||||
- [ ] GitHub Actions for tests and linters
|
||||
- [ ] Swagger API
|
||||
- [X] GitHub Actions for tests and linters
|
||||
- [ ] Basic List pages for entities
|
||||
- [X] Swagger API
|
||||
- [ ] Local development tool to clear/seed database
|
||||
...
|
||||
- [ ] Authentication (will need to link `user` table to `player`)
|
||||
@ -41,3 +42,8 @@ https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#create-the-tab
|
||||
# Authentication
|
||||
|
||||
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_sqlalchemy import SQLAlchemy
|
||||
from flasgger import Swagger
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config["SWAGGER"] = {"openapi": "3.0.2"}
|
||||
swagger = Swagger(app)
|
||||
|
||||
secret_key = os.environ.get("SECRET_KEY")
|
||||
if not secret_key:
|
||||
|
194
app/main.py
194
app/main.py
@ -7,20 +7,93 @@ main = Blueprint("main", __name__)
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
"""Main Page
|
||||
---
|
||||
responses:
|
||||
200:
|
||||
description: A friendly greeting
|
||||
schema:
|
||||
type: string
|
||||
"""
|
||||
return "Hello, World - but new!"
|
||||
|
||||
|
||||
@main.route("/player", methods=["POST"])
|
||||
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
|
||||
player = Player(name=data["name"])
|
||||
db.session.add(player)
|
||||
db.session.commit()
|
||||
return {"id": player.id}
|
||||
return {"id": player.id}, 201
|
||||
|
||||
|
||||
@main.route("/player/<player_id>")
|
||||
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))
|
||||
if not player_from_db:
|
||||
return "Not Found", 404
|
||||
@ -36,6 +109,29 @@ def get_player(player_id: str):
|
||||
|
||||
@main.route("/player/<player_id>", methods=["DELETE"])
|
||||
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
|
||||
# that `DELETE` should be idempotent.
|
||||
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"])
|
||||
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
|
||||
owner_id = data["owner_id"]
|
||||
|
||||
@ -58,11 +191,45 @@ def create_deck():
|
||||
db.session.add(deck)
|
||||
db.session.commit()
|
||||
print("Finished creating the deck!")
|
||||
return {"id": deck.id}
|
||||
return {"id": deck.id}, 201
|
||||
|
||||
|
||||
@main.route("/deck/<deck_id>")
|
||||
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))
|
||||
if not deck_from_db:
|
||||
return "Not Found", 404
|
||||
@ -81,6 +248,29 @@ def get_deck(deck_id: str):
|
||||
|
||||
@main.route("/deck/<deck_id>", methods=["DELETE"])
|
||||
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.commit()
|
||||
return "", 204
|
||||
|
@ -12,7 +12,9 @@ def test_add_and_retrieve_player(client: FlaskClient):
|
||||
response = _json_get(client, "/player/1")
|
||||
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")
|
||||
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"
|
||||
|
||||
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"]
|
||||
|
||||
create_deck_response = _json_post(
|
||||
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?
|
||||
deck_id = create_deck_response.json["id"]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user