Setup Rest API with Swagger #50

This commit is contained in:
Daniel Grams 2021-01-09 20:23:41 +01:00
parent 8fb3b83530
commit 1a5f33c9de
27 changed files with 384 additions and 51 deletions

View File

@ -1,6 +1,6 @@
language: python
python:
- "3.7"
- "3.8"
addons:
apt:
packages:

View File

@ -50,12 +50,13 @@ cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
# API
rest_api = Api(app, "/api/v1")
marshmallow = Marshmallow(app)
marshmallow_plugin = MarshmallowPlugin()
app.config.update(
{
"APISPEC_SPEC": APISpec(
title="Oveda API",
version="1.0.0",
plugins=[MarshmallowPlugin()],
plugins=[marshmallow_plugin],
openapi_version="2.0",
),
}

View File

@ -1,3 +1,22 @@
def enum_to_properties(self, field, **kwargs):
"""
Add an OpenAPI extension for marshmallow_enum.EnumField instances
"""
import marshmallow_enum
if isinstance(field, marshmallow_enum.EnumField):
return {"type": "string", "enum": [m.name for m in field.enum]}
return {}
from project import marshmallow_plugin
marshmallow_plugin.converter.add_attribute_function(enum_to_properties)
import project.api.event.resources
import project.api.event_date.resources
import project.api.image.resources
import project.api.location.resources
import project.api.organization.resources
import project.api.organizer.resources
import project.api.place.resources

View File

@ -1,6 +1,12 @@
from project import marshmallow
from marshmallow import fields
from project.models import Event
from marshmallow_enum import EnumField
from project.models import Event, EventStatus
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
from project.api.organization.schemas import OrganizationRefSchema
from project.api.organizer.schemas import OrganizerRefSchema
from project.api.image.schemas import ImageRefSchema
from project.api.place.schemas import PlaceRefSchema
class EventSchema(marshmallow.SQLAlchemySchema):
@ -8,7 +14,11 @@ class EventSchema(marshmallow.SQLAlchemySchema):
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()
description = marshmallow.auto_field()
external_link = marshmallow.auto_field()
ticket_link = marshmallow.auto_field()
@ -17,14 +27,28 @@ class EventSchema(marshmallow.SQLAlchemySchema):
accessible_for_free = marshmallow.auto_field()
age_from = marshmallow.auto_field()
age_to = marshmallow.auto_field()
status = EnumField(EventStatus)
organization = marshmallow.HyperlinkRelated(
"organizationresource", attribute="admin_unit"
)
organizer = marshmallow.URLFor(
"organizerresource",
values=dict(organization_id="<admin_unit_id>", organizer_id="<organizer_id>"),
)
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
organizer = fields.Nested(OrganizerRefSchema)
photo = fields.Nested(ImageRefSchema)
place = fields.Nested(PlaceRefSchema, attribute="event_place")
class EventRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Event
id = marshmallow.auto_field()
href = marshmallow.URLFor("eventresource", values=dict(id="<id>"))
name = marshmallow.auto_field()
description = marshmallow.auto_field()
status = EnumField(EventStatus)
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
organizer = fields.Nested(OrganizerRefSchema)
photo = fields.Nested(ImageRefSchema)
place = fields.Nested(PlaceRefSchema, attribute="event_place")
class EventListItemSchema(marshmallow.SQLAlchemySchema):
@ -39,41 +63,11 @@ class EventListItemSchema(marshmallow.SQLAlchemySchema):
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 EventListRequestSchema(PaginationRequestSchema):
pass
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"},
)
class EventListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventListItemSchema), metadata={"description": "Events"}
)

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_date.schemas import (
EventDateSchema,
EventDateListRequestSchema,
EventDateListResponseSchema,
)
from project.models import EventDate
class EventDateListResource(MethodResource):
@doc(tags=["Event Dates"])
@use_kwargs(EventDateListRequestSchema, location=("query"))
@marshal_with(EventDateListResponseSchema)
def get(self, **kwargs):
pagination = EventDate.query.paginate()
return pagination
class EventDateResource(MethodResource):
@doc(tags=["Event Dates"])
@marshal_with(EventDateSchema)
def get(self, id):
return EventDate.query.get_or_404(id)
rest_api.add_resource(EventDateListResource, "/event_dates")
api_docs.register(EventDateListResource)
rest_api.add_resource(EventDateResource, "/event_dates/<int:id>")
api_docs.register(EventDateResource)

View File

@ -0,0 +1,36 @@
from project import marshmallow
from marshmallow import fields
from project.models import EventDate
from project.api.event.schemas import EventSchema, EventRefSchema
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
class EventDateSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventDate
id = marshmallow.auto_field()
start = marshmallow.auto_field()
end = marshmallow.auto_field()
event = fields.Nested(EventSchema)
class EventDateListItemSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventDate
id = marshmallow.auto_field()
href = marshmallow.URLFor("eventdateresource", values=dict(id="<id>"))
start = marshmallow.auto_field()
end = marshmallow.auto_field()
event = fields.Nested(EventRefSchema)
class EventDateListRequestSchema(PaginationRequestSchema):
pass
class EventDateListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventDateListItemSchema), metadata={"description": "Dates"}
)

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.image.schemas import ImageSchema
from project.models import AdminUnit
class ImageResource(MethodResource):
@doc(tags=["Images"])
@marshal_with(ImageSchema)
def get(self, id):
return AdminUnit.query.get_or_404(id)
rest_api.add_resource(ImageResource, "/images/<int:id>")
api_docs.register(ImageResource)

View File

@ -0,0 +1,24 @@
from project import marshmallow
from project.models import Image
class ImageSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Image
id = marshmallow.auto_field()
copyright_text = marshmallow.auto_field()
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
class ImageRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Image
id = marshmallow.auto_field()
copyright_text = marshmallow.auto_field()
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
href = marshmallow.URLFor(
"imageresource",
values=dict(id="<id>"),
)

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.location.schemas import LocationSchema
from project.models import Location
class LocationResource(MethodResource):
@doc(tags=["Locations"])
@marshal_with(LocationSchema)
def get(self, id):
return Location.query.get_or_404(id)
rest_api.add_resource(LocationResource, "/locations/<int:id>")
api_docs.register(LocationResource)

View File

@ -0,0 +1,30 @@
from marshmallow import fields
from project import marshmallow
from project.models import Location
class LocationSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Location
id = marshmallow.auto_field()
street = marshmallow.auto_field()
postalCode = marshmallow.auto_field()
city = marshmallow.auto_field()
state = marshmallow.auto_field()
country = marshmallow.auto_field()
longitude = fields.Str()
latitude = fields.Str()
class LocationRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Location
id = marshmallow.auto_field()
longitude = fields.Str()
latitude = fields.Str()
href = marshmallow.URLFor(
"locationresource",
values=dict(id="<id>"),
)

View File

@ -12,3 +12,12 @@ class OrganizationSchema(marshmallow.SQLAlchemySchema):
email = marshmallow.auto_field()
phone = marshmallow.auto_field()
fax = marshmallow.auto_field()
class OrganizationRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = AdminUnit
id = marshmallow.auto_field()
name = marshmallow.auto_field()
href = marshmallow.URLFor("organizationresource", values=dict(id="<id>"))

View File

@ -8,12 +8,12 @@ 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)
def get(self, id):
return EventOrganizer.query.get_or_404(id)
rest_api.add_resource(
OrganizerResource,
"/organizations/<int:organization_id>/organizers/<int:organizer_id>",
"/organizers/<int:id>",
)
api_docs.register(OrganizerResource)

View File

@ -8,3 +8,15 @@ class OrganizerSchema(marshmallow.SQLAlchemySchema):
id = marshmallow.auto_field()
name = marshmallow.auto_field()
class OrganizerRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventOrganizer
id = marshmallow.auto_field()
name = marshmallow.auto_field()
href = marshmallow.URLFor(
"organizerresource",
values=dict(id="<id>"),
)

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.place.schemas import PlaceSchema
from project.models import EventPlace
class PlaceResource(MethodResource):
@doc(tags=["Places"])
@marshal_with(PlaceSchema)
def get(self, id):
return EventPlace.query.get_or_404(id)
rest_api.add_resource(
PlaceResource,
"/places/<int:id>",
)
api_docs.register(PlaceResource)

View File

@ -0,0 +1,30 @@
from marshmallow import fields
from project import marshmallow
from project.models import EventPlace
from project.api.image.schemas import ImageRefSchema
from project.api.location.schemas import LocationRefSchema
class PlaceSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventPlace
id = marshmallow.auto_field()
name = marshmallow.auto_field()
url = marshmallow.auto_field()
description = marshmallow.auto_field()
photo = fields.Nested(ImageRefSchema)
location = fields.Nested(LocationRefSchema)
class PlaceRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventPlace
id = marshmallow.auto_field()
name = marshmallow.auto_field()
href = marshmallow.URLFor(
"placeresource",
values=dict(id="<id>"),
)
location = fields.Nested(LocationRefSchema)

39
project/api/schemas.py Normal file
View File

@ -0,0 +1,39 @@
from project import marshmallow
from marshmallow import fields
class PaginationRequestSchema(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 PaginationResponseSchema(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"},
)

View File

@ -52,6 +52,7 @@ jsonschema==3.2.0
Mako==1.1.3
MarkupSafe==1.1.1
marshmallow==3.10.0
marshmallow-enum==1.5.1
marshmallow-sqlalchemy==0.24.1
mccabe==0.6.1
mistune==0.8.4

View File

@ -1,9 +1,20 @@
def test_read(client, seeder, utils):
def test_read(client, app, db, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
event_id = seeder.create_event(admin_unit_id)
with app.app_context():
from project.models import Event, EventStatus
from project.services.event import update_event
event = Event.query.get(event_id)
event.status = EventStatus.scheduled
update_event(event)
db.session.commit()
url = utils.get_url("eventresource", id=event_id)
utils.get_ok(url)
response = utils.get_ok(url)
assert response.json["status"] == "scheduled"
def test_list(client, seeder, utils):

View File

@ -0,0 +1,14 @@
def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventdateresource", id=1)
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("eventdatelistresource")
utils.get_ok(url)

6
tests/api/test_image.py Normal file
View File

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

View File

@ -0,0 +1,20 @@
def test_read(client, app, db, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
with app.app_context():
from project.models import Location
location = Location()
location.street = "Markt 7"
location.postalCode = "38640"
location.city = "Goslar"
location.latitude = 51.9077888
location.longitude = 10.4333312
db.session.add(location)
db.session.commit()
location_id = location.id
url = utils.get_url("locationresource", id=location_id)
response = utils.get_ok(url)
assert response.json["latitude"] == "51.9077888000000000"

View File

@ -2,7 +2,5 @@ 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
)
url = utils.get_url("organizerresource", id=organizer_id)
utils.get_ok(url)

6
tests/api/test_place.py Normal file
View File

@ -0,0 +1,6 @@
def test_read(client, app, db, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
place_id = seeder.upsert_default_event_place(admin_unit_id)
url = utils.get_url("placeresource", id=place_id)
utils.get_ok(url)