From 7c3d25a87a4c32b8616944ae7aceeb8e29bd70b0 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Wed, 22 Jun 2016 13:48:59 +0100 Subject: [PATCH] Publish a Swagger specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new endpoint (`/spec`) which returns a the specification of the API in Swagger-formatted JSON. This means we will have something to point frontends at, so we can evaluate which ones we like. Right now it’s all hand-defined. If we were consistent about our use of Marshmallow we could generated the spec from the Marshmallow schemas. --- app/__init__.py | 5 +- app/spec/__init__.py | 0 app/spec/rest.py | 328 ++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + requirements_for_test.txt | 1 + tests/app/spec/test_rest.py | 18 ++ 6 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 app/spec/__init__.py create mode 100644 app/spec/rest.py create mode 100644 tests/app/spec/test_rest.py diff --git a/app/__init__.py b/app/__init__.py index daf382713..77664fc34 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -70,6 +70,7 @@ def create_app(app_name=None): from app.template_statistics.rest import template_statistics as template_statistics_blueprint from app.events.rest import events as events_blueprint from app.provider_details.rest import provider_details as provider_details_blueprint + from app.spec.rest import spec as spec_blueprint application.register_blueprint(service_blueprint, url_prefix='/service') application.register_blueprint(user_blueprint, url_prefix='/user') @@ -83,6 +84,7 @@ def create_app(app_name=None): application.register_blueprint(template_statistics_blueprint) application.register_blueprint(events_blueprint) application.register_blueprint(provider_details_blueprint, url_prefix='/provider-details') + application.register_blueprint(spec_blueprint, url_prefix='/spec') return application @@ -95,7 +97,8 @@ def init_app(app): url_for('notifications.process_ses_response'), url_for('notifications.process_firetext_response'), url_for('notifications.process_mmg_response'), - url_for('status.show_delivery_status') + url_for('status.show_delivery_status'), + url_for('spec.get_spec') ] if request.path not in no_auth_req: from app.authentication import auth diff --git a/app/spec/__init__.py b/app/spec/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/spec/rest.py b/app/spec/rest.py new file mode 100644 index 000000000..400cb5849 --- /dev/null +++ b/app/spec/rest.py @@ -0,0 +1,328 @@ +from flask import jsonify, current_app, Blueprint +from apispec import APISpec + + +spec = Blueprint('spec', __name__) + + +api_spec = APISpec( + title='GOV.UK Notify', + version='0.0.0' +) + +api_spec.definition('NotificationStatusSchema', properties={ + "content_char_count": { + "format": "int32", + "type": "integer" + }, + "created_at": { + "format": "date-time", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "job": { + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "original_file_name": { + "type": "string" + } + }, + "required": [ + "id", + "original_file_name" + ], + "type": "object" + }, + "job_row_number": { + "format": "int32", + "type": "integer" + }, + "reference": { + "type": "string" + }, + "sent_at": { + "format": "date-time", + "type": "string" + }, + "sent_by": { + "type": "string" + }, + "service": { + "type": "string" + }, + "status": { + "enum": [ + "delivered", + "sending", + "technical-failure", + "temporary-failure", + "permanent-failure", + "pending", + "failed" + ], + "type": "string" + }, + "template": { + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "name": { + "type": "string" + }, + "template_type": { + "enum": [ + "sms", + "email", + "letter" + ], + "type": "string" + } + }, + "required": [ + "template_type", + "name" + ], + "type": "object" + }, + "template_version": { + "format": "int32", + "type": "integer" + }, + "to": { + "type": "string" + }, + "updated_at": { + "format": "date-time", + "type": "string" + } +}) +api_spec.definition('NotificationSchema', properties={ + "notification": { + "$ref": "#/definitions/NotificationStatusSchema" + } +}) +api_spec.definition('NotificationsSchema', properties={ + "notifications": { + "type": "array", + "items": { + "$ref": "#/definitions/NotificationStatusSchema" + } + } +}) +api_spec.definition('NotificationSentSchema', properties={ + "body": { + "description": "The content of the message", + "type": "string" + }, + "notification": { + "properties": { + "id": { + "type": "string" + } + }, + "type": "object" + }, + "subject": { + "description": "The subject of the email (present for email notifications only)", + "type": "string" + }, + "template_version": { + "description": "The version number of the template that was used", + "type": "integer" + } +}) +api_spec.definition('Error', properties={ + 'result': { + 'type': 'string', + 'description': 'will say ‘error’' + }, + 'message': { + 'type': 'string', + 'description': 'description of the error' + } +}) +api_spec.add_path(path="/notifications", operations={ + "get": { + "parameters": [ + { + "description": "page number", + "in": "query", + "name": "page", + "type": "integer" + }, + { + "default": 50, + "description": "number of notifications per page", + "in": "query", + "name": "page_size", + "type": "integer" + }, + { + "default": 7, + "description": "number of days", + "in": "query", + "name": "limit_days", + "type": "integer" + }, + { + "description": "sms or email", + "enum": [ + "sms", + "email" + ], + "in": "query", + "name": "template_type", + "type": "string" + }, + { + "description": "sms or email", + "in": "query", + "name": "status", + "type": "string" + } + ], + "responses": { + "200": { + "description": "Notifications found", + "schema": { + "$ref": "#/definitions/NotificationsSchema" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authorisation header is missing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Invalid or expired token", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "No notifications found" + } + } + } +}) +api_spec.add_path(path="/notification/{notification_id_or_type}", operations={ + "get": { + "parameters": [ + { + "description": "16 character UUID", + "in": "path", + "name": "notification_id_or_type", + "required": True, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Found", + "schema": { + "$ref": "#/definitions/NotificationSchema" + } + }, + "401": { + "description": "Authorisation header is missing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Invalid or expired token", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not found" + } + } + }, + "post": { + "description": "Send a single email or text message", + "parameters": [ + { + "description": "email or sms", + "in": "path", + "name": "notification_id_or_type", + "pattern": "[email|sms]", + "required": True, + "type": "string" + }, + { + "description": "the recipient's phone number or email address", + "in": "formData", + "name": "to", + "required": True, + "type": "string" + }, + { + "description": "the ID of the template to use", + "in": "formData", + "name": "template", + "required": True, + "type": "string" + }, + { + "description": "specifies the placeholders and values in your templates", + "in": "formData", + "name": "personalisation", + "type": "string" + } + ], + "responses": { + "200": { + "description": "Notification sent", + "schema": { + "$ref": "#/definitions/NotificationSentSchema" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authorisation header is missing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Invalid or expired token", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "You have reached the maximum number of messages you can send per day", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } +}) + + +@spec.route('') +def get_spec(): + return jsonify(api_spec.to_dict()) diff --git a/requirements.txt b/requirements.txt index ce26322e4..300c539f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +apispec==0.12.0 bleach==1.4.2 Flask==0.10.1 Flask-Script==2.0.5 diff --git a/requirements_for_test.txt b/requirements_for_test.txt index de9a294b1..f5db40330 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -6,5 +6,6 @@ pytest-cov==2.2.0 coveralls==1.1 mock==1.0.1 moto==0.4.19 +flex==5.6.0 freezegun==0.3.6 requests-mock==0.7.0 diff --git a/tests/app/spec/test_rest.py b/tests/app/spec/test_rest.py new file mode 100644 index 000000000..03b942a13 --- /dev/null +++ b/tests/app/spec/test_rest.py @@ -0,0 +1,18 @@ +import flex +import pytest + +from flask import json +from tests import create_authorization_header + + +def test_spec_returns_valid_json(notify_api, sample_notification): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header(service_id=sample_notification.service_id) + + response = client.get('/spec', headers=[auth_header]) + + # Check that it’s a valid Swagger schema + flex.load( + json.loads(response.get_data(as_text=True)) + )