Setup Rest API with Swagger #50

This commit is contained in:
Daniel Grams 2021-01-07 14:35:05 +01:00
parent 57561e9b03
commit 8fb3b83530
18 changed files with 247 additions and 1 deletions

View File

@ -10,6 +10,11 @@ from flask_cors import CORS
from flask_qrcode import QRcode
from flask_mail import Mail, email_dispatched
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow
from flask_restful import Api
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask_apispec.extension import FlaskApiSpec
# Create app
app = Flask(__name__)
@ -42,6 +47,21 @@ babel = Babel(app)
# cors
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
# API
rest_api = Api(app, "/api/v1")
marshmallow = Marshmallow(app)
app.config.update(
{
"APISPEC_SPEC": APISpec(
title="Oveda API",
version="1.0.0",
plugins=[MarshmallowPlugin()],
openapi_version="2.0",
),
}
)
api_docs = FlaskApiSpec(app)
# Mail
mail_server = os.getenv("MAIL_SERVER")
@ -114,6 +134,9 @@ from project.views import (
widget,
)
# API Resources
import project.api
# Command line
import project.cli.event
import project.cli.user

3
project/api/__init__.py Normal file
View File

@ -0,0 +1,3 @@
import project.api.event.resources
import project.api.organization.resources
import project.api.organizer.resources

View File

View File

@ -0,0 +1,32 @@
from project import rest_api, api_docs
from flask_apispec import marshal_with, doc, use_kwargs
from flask_apispec.views import MethodResource
from project.api.event.schemas import (
EventSchema,
EventListRequestSchema,
EventListResponseSchema,
)
from project.models import Event
class EventListResource(MethodResource):
@doc(tags=["Events"])
@use_kwargs(EventListRequestSchema, location=("query"))
@marshal_with(EventListResponseSchema)
def get(self, **kwargs):
pagination = Event.query.paginate()
return pagination
class EventResource(MethodResource):
@doc(tags=["Events"])
@marshal_with(EventSchema)
def get(self, id):
return Event.query.get_or_404(id)
rest_api.add_resource(EventListResource, "/events")
api_docs.register(EventListResource)
rest_api.add_resource(EventResource, "/events/<int:id>")
api_docs.register(EventResource)

View File

@ -0,0 +1,79 @@
from project import marshmallow
from marshmallow import fields
from project.models import Event
class EventSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Event
id = marshmallow.auto_field()
name = marshmallow.auto_field()
description = marshmallow.auto_field()
external_link = marshmallow.auto_field()
ticket_link = marshmallow.auto_field()
tags = marshmallow.auto_field()
kid_friendly = marshmallow.auto_field()
accessible_for_free = marshmallow.auto_field()
age_from = marshmallow.auto_field()
age_to = marshmallow.auto_field()
organization = marshmallow.HyperlinkRelated(
"organizationresource", attribute="admin_unit"
)
organizer = marshmallow.URLFor(
"organizerresource",
values=dict(organization_id="<admin_unit_id>", organizer_id="<organizer_id>"),
)
class EventListItemSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Event
id = marshmallow.auto_field()
href = marshmallow.URLFor("eventresource", values=dict(id="<id>"))
name = marshmallow.auto_field()
start = marshmallow.auto_field()
end = marshmallow.auto_field()
recurrence_rule = marshmallow.auto_field()
class EventListRequestSchema(marshmallow.Schema):
page = fields.Integer(
required=False,
default=1,
metadata={"description": "The page number (1 indexed)."},
)
per_page = fields.Integer(
required=False, default=20, metadata={"description": "Items per page"}
)
class EventListResponseSchema(marshmallow.Schema):
has_next = fields.Boolean(
required=True, metadata={"description": "True if a next page exists."}
)
has_prev = fields.Boolean(
required=True, metadata={"description": "True if a previous page exists."}
)
next_num = fields.Integer(
required=False, metadata={"description": "Number of the next page."}
)
prev_num = fields.Integer(
required=False, metadata={"description": "Number of the previous page."}
)
page = fields.Integer(
required=True, metadata={"description": "The current page number (1 indexed)."}
)
pages = fields.Integer(
required=True, metadata={"description": "The total number of pages."}
)
per_page = fields.Integer(required=True, metadata={"description": "Items per page"})
total = fields.Integer(
required=True,
metadata={"description": "The total number of items matching the query"},
)
items = fields.List(
fields.Nested(EventListItemSchema), metadata={"description": "Events"}
)

View File

View File

@ -0,0 +1,16 @@
from project import rest_api, api_docs
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.organization.schemas import OrganizationSchema
from project.models import AdminUnit
class OrganizationResource(MethodResource):
@doc(tags=["Organizations"])
@marshal_with(OrganizationSchema)
def get(self, id):
return AdminUnit.query.get_or_404(id)
rest_api.add_resource(OrganizationResource, "/organizations/<int:id>")
api_docs.register(OrganizationResource)

View File

@ -0,0 +1,14 @@
from project import marshmallow
from project.models import AdminUnit
class OrganizationSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = AdminUnit
id = marshmallow.auto_field()
name = marshmallow.auto_field()
url = marshmallow.auto_field()
email = marshmallow.auto_field()
phone = marshmallow.auto_field()
fax = marshmallow.auto_field()

View File

View File

@ -0,0 +1,19 @@
from project import rest_api, api_docs
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.organizer.schemas import OrganizerSchema
from project.models import EventOrganizer
class OrganizerResource(MethodResource):
@doc(tags=["Organizers"])
@marshal_with(OrganizerSchema)
def get(self, organization_id, organizer_id):
return EventOrganizer.query.get_or_404(organizer_id)
rest_api.add_resource(
OrganizerResource,
"/organizations/<int:organization_id>/organizers/<int:organizer_id>",
)
api_docs.register(OrganizerResource)

View File

@ -0,0 +1,10 @@
from project import marshmallow
from project.models import EventOrganizer
class OrganizerSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventOrganizer
id = marshmallow.auto_field()
name = marshmallow.auto_field()

View File

@ -29,7 +29,7 @@ from sqlalchemy import and_
def _current_user_id_or_none():
if current_user.is_authenticated:
if current_user and current_user.is_authenticated:
return current_user.id
return None

View File

@ -1,4 +1,7 @@
alembic==1.4.3
aniso8601==8.1.0
apispec==4.0.0
apispec-webframeworks==0.5.2
appdirs==1.4.4
attrs==20.3.0
Babel==2.9.0
@ -22,15 +25,18 @@ email-validator==1.1.2
filelock==3.0.12
flake8==3.8.4
Flask==1.1.2
flask-apispec==0.11.0
Flask-BabelEx==0.9.4
Flask-Bootstrap==3.3.7.1
Flask-Cors==3.0.9
Flask-Dance==3.2.0
Flask-Login==0.5.0
Flask-Mail==0.9.1
flask-marshmallow==0.14.0
Flask-Migrate==2.5.3
Flask-Principal==0.4.0
Flask-QRcode==3.0.0
Flask-RESTful==0.3.8
Flask-Security-Too==3.4.4
Flask-SQLAlchemy==2.4.4
Flask-WTF==0.14.3
@ -42,9 +48,13 @@ importlib-metadata==3.1.1
iniconfig==1.1.1
itsdangerous==1.1.0
Jinja2==2.11.2
jsonschema==3.2.0
Mako==1.1.3
MarkupSafe==1.1.1
marshmallow==3.10.0
marshmallow-sqlalchemy==0.24.1
mccabe==0.6.1
mistune==0.8.4
mypy-extensions==0.4.3
nodeenv==1.5.0
oauthlib==3.1.0
@ -60,6 +70,7 @@ pycodestyle==2.6.0
pycparser==2.20
pyflakes==2.2.0
pyparsing==2.4.7
pyrsistent==0.17.3
pytest==6.1.2
pytest-cov==2.10.1
pytest-mock==3.3.1
@ -78,6 +89,7 @@ soupsieve==2.1
speaklater==1.3
SQLAlchemy==1.3.20
SQLAlchemy-Utils==0.36.8
swagger-spec-validator==2.7.3
toml==0.10.2
typed-ast==1.4.1
typing-extensions==3.7.4.3
@ -85,6 +97,7 @@ urllib3==1.26.2
URLObject==2.4.3
virtualenv==20.2.2
visitor==0.1.3
webargs==7.0.1
Werkzeug==1.0.1
WTForms==2.3.3
WTForms-SQLAlchemy==0.2

0
tests/api/__init__.py Normal file
View File

14
tests/api/test_event.py Normal file
View File

@ -0,0 +1,14 @@
def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
event_id = seeder.create_event(admin_unit_id)
url = utils.get_url("eventresource", id=event_id)
utils.get_ok(url)
def test_list(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventlistresource")
utils.get_ok(url)

View File

@ -0,0 +1,5 @@
def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
url = utils.get_url("organizationresource", id=admin_unit_id)
utils.get_ok(url)

View File

@ -0,0 +1,8 @@
def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
url = utils.get_url(
"organizerresource", organization_id=admin_unit_id, organizer_id=organizer_id
)
utils.get_ok(url)

10
tests/api/test_swagger.py Normal file
View File

@ -0,0 +1,10 @@
from swagger_spec_validator import validator20
def test_swagger(utils):
response = utils.get_ok("/swagger/")
validator20.validate_spec(response.json)
def test_swagger_ui(utils):
utils.get_ok("/swagger-ui/")