diff --git a/.gitignore b/.gitignore index 5d4d1db..cddc94a 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,8 @@ ENV/ # serverless .serverless/ .requirements/ + +# codegen +sdk/ + +node_modules/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bfe18a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ + +SDK_DIR=sdk + +$(SDK_DIR): + mkdir -p $(SDK_DIR) + +generate-openapi: $(SDK_DIR) + $(shell serverless invoke local -f openapi > $(SDK_DIR)/openapi.json) + +generate-client: generate-openapi +ifdef lang + mkdir -p $(SDK_DIR)/$(lang) + docker run --rm -v "$(PWD)/$(SDK_DIR):/local" openapitools/openapi-generator-cli generate \ + -i /local/openapi.json \ + -g $(lang) \ + -o /local/$(lang) + @echo "SDK output to $(SDK_DIR)/$(lang)" +else + @echo "Please specify language with lang=" + @echo "Example: make lang=typescript-axios generate-client" +endif diff --git a/README.md b/README.md index f5c01fb..c7d5ca1 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,20 @@ # Serverless-Flask +### API edition. The fastest way to a Flask application with [Serverless](https://github.com/serverless/serverless). -## Usage +This version includes APISpec and Marshmallow for easy declaration of input and output for API functions +and automatic generation of swagger. CORS is enabled and can be customized. +## Usage +* [Install yarn](https://yarnpkg.com/lang/en/docs/install/#mac-stable) if you don't have it ``` -$ npm install -g serverless -$ serverless install --url https://github.com/alexdebrie/serverless-flask --name my-flask-app -$ cd my-flask-app && npm run setup - -$ serverless deploy +yarn global install serverless +sls install --url https://github.com/revmischa/serverless-flask --name my-flask-app +cd my-flask-app +yarn setup +# +sls deploy ``` Once the deploy is complete, run `sls info` to get the endpoint: @@ -19,8 +24,8 @@ $ sls info Service Information endpoints: - ANY - https://abc6defghi.execute-api.us-east-1.amazonaws.com/dev <-- Endpoint - ANY - https://abc6defghi.execute-api.us-east-1.amazonaws.com/dev/{proxy+} + ANY - https://abc6defghi.execute-api.eu-central-1.amazonaws.com/dev <-- Endpoint + ANY - https://abc6defghi.execute-api.eu-central-1.amazonaws.com/dev/{proxy+} ``` Copy paste into your browser, and _voila_! @@ -44,13 +49,23 @@ sls wsgi serve * Debugger is active! ``` -Navigate to [localhost:5000](http://localhost:5000) to see your app running locally. +Navigate to [localhost:5000](http://localhost:5000) to see swagger UI for your API. + + +## Client CodeGen + +Want to generate a client library for your API? No problem. + +* Run `make generate-client lang=$LANG` + +Where `$LANG` can be any language supported by [OpenAPI-Generator](https://github.com/openapitools/openapi-generator#overview). + +e.g. `lang=go` or `lang=typescript-axios`. + ## Configuration The `postsetup.js` prompt will walk you through some setup questions that may be custom to your use case. This includes: - -- Python runtime version; - Whether you have Docker setup, which assists in packaging dependencies. For more info, check out [this post on managing your Python packages with Serverless](https://serverless.com/blog/serverless-python-packaging/); - Whether you want to set up a custom domain that you own, rather than a random assigned one from AWS. For more details on that, look at [this post on setting up a custom domain with API Gateway and Serverless](https://serverless.com/blog/serverless-api-gateway-domain/). diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e23d45d --- /dev/null +++ b/api/__init__.py @@ -0,0 +1 @@ +"""API endpoints.""" diff --git a/api/doc.py b/api/doc.py new file mode 100644 index 0000000..61052a5 --- /dev/null +++ b/api/doc.py @@ -0,0 +1,26 @@ +"""Generate OpenAPI documentation.""" +from flask import Flask +from flask_apispec.extension import FlaskApiSpec, make_apispec + + +class ServerlessOpenAPI: + def __init__(self, app: Flask) -> None: + app.config.update({ + 'APISPEC_SPEC': make_apispec(title='Flask API', version='v1'), + 'APISPEC_SWAGGER_URL': '/swagger/', + }) + self.apispec = FlaskApiSpec(app) + + def register_all(self): + """Generate documentation. + + Call after all views loaded. + """ + self.apispec.register_existing_resources() + + +# lambda function +def get_openapi(event, context): + """Get raw OpenAPI v2 API description.""" + from app import docs + return docs.apispec.spec.to_dict() diff --git a/api/index.py b/api/index.py new file mode 100644 index 0000000..c01a1c6 --- /dev/null +++ b/api/index.py @@ -0,0 +1,16 @@ +from flask_apispec import use_kwargs, marshal_with +from marshmallow import Schema, fields +from app import app +from typing import Optional + + +class IndexSchema(Schema): + name: Optional[str] = fields.Str(required=False) + + +@app.route('/api/', methods=['POST']) +@use_kwargs(IndexSchema(strict=True)) +@marshal_with(IndexSchema) +def api_index(name: str = None): + """Main API endpoint.""" + return {'name': f'you entered {name}'} diff --git a/app.py b/app.py index 6b2d107..e0d10b8 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,24 @@ -from flask import Flask, render_template +"""Lambda entry point.""" +from flask import Flask, redirect, url_for +from flask_cors import CORS +from werkzeug.contrib.fixers import ProxyFix +from api.doc import ServerlessOpenAPI +# create app app = Flask(__name__) +CORS(app) +app.wsgi_app = ProxyFix(app.wsgi_app) +docs = ServerlessOpenAPI(app) + +# set up views +import api.index +docs.register_all() + @app.route("/") def main(): - return render_template('index.html') + """Index page.""" + return redirect(url_for('flask-apispec.swagger-ui')) if __name__ == "__main__": diff --git a/package.json b/package.json index 9b51b99..c2b9966 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "setup": "npm install", + "setup": "yarn install", "postsetup": "node postsetup.js", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -14,7 +14,9 @@ "chalk": "^2.1.0", "inquirer": "^3.2.3", "js-yaml": "^3.9.1", - "serverless-python-requirements": "^2.5.0", + "serverless-domain-manager": "^2.6.6", + "serverless-python-requirements": "^4.2.5", "serverless-wsgi": "^1.3.0" - } + }, + "dependencies": {} } diff --git a/postsetup.js b/postsetup.js index 69cbef8..e4b00ef 100644 --- a/postsetup.js +++ b/postsetup.js @@ -1,4 +1,4 @@ -// Check to see if the user has Docker installed and which version of Python they prefer. +// Check to see if the user has Docker installed 'use strict'; var inquirer = require('inquirer'); @@ -9,15 +9,6 @@ var fs = require('fs'); console.log(chalk.yellow('Hi, a few quick questions before we start:')); var questions = [ - { - type: 'list', - name: 'python', - message: 'What Python version?', - choices: ['python2.7', 'python3.6'], - filter: function (val) { - return val.toLowerCase(); - } - }, { type: 'confirm', name: 'docker', @@ -41,10 +32,9 @@ var questions = [ ] inquirer.prompt(questions).then(function (answers) { - + var doc = YAML.safeLoad(fs.readFileSync('serverless.yml', 'utf8')); doc.custom.pythonRequirements.dockerizePip = answers.docker; - doc.provider.runtime = answers.python; if (answers.wantsDomain) { doc.plugins.push('serverless-domain-manager'); doc.custom.customDomain = createCustomDomain(answers.domainName); diff --git a/requirements.txt b/requirements.txt index 6aee249..a2502de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -click==6.7 -Flask==0.12.2 -itsdangerous==0.24 -Jinja2==2.9.6 -MarkupSafe==1.0 -Werkzeug==0.12.2 +Flask +flask_cors +boto3 +# flask_apispec +# use fixed SERVER_NAME https://github.com/revmischa/flask-apispec/commits/script_root +git+https://github.com/revmischa/flask-apispec.git@7cf50d33189873f498b4474610e32896247a1260#egg=flask_apispec +apispec +marshmallow diff --git a/serverless.yml b/serverless.yml index 90e600b..e79061e 100644 --- a/serverless.yml +++ b/serverless.yml @@ -18,9 +18,9 @@ package: provider: name: aws - runtime: python2.7 + runtime: python3.6 stage: dev - region: us-east-1 + region: eu-central-1 functions: app: @@ -28,3 +28,7 @@ functions: events: - http: ANY / - http: 'ANY {proxy+}' + openapi: + handler: api.doc.get_openapi + events: + - http: GET /openapi diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 6a6174e..0000000 --- a/templates/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - Serverless Flask App - - - - - - -
-
- -

🐍 Serverless Flask

-
-
-

Congrats on deploying your Flask App 🎉

-

- There are many things you can build with Serverless & Python. The limit is your imagination! -

-

- Here are a couple places we recommend starting: -

-
    -
  1. Set a custom domain
  2. -
  3. Add a DynamoDB table
  4. -
  5. Use secret variables
  6. -
-
-
-

- This project is powered by - -

-
-
-
-

Serverless WSGI:

-

Deploy WSGI apps to AWS Lambda
Read More

-
-
-

Serverless Python Requirements:

-

Utility for compiling Python libs with your functions
Read More

-
-
-
-

© Serverless 2017

-
-
- - - diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..048719d --- /dev/null +++ b/tox.ini @@ -0,0 +1,3 @@ +[flake8] +ignore = E402,E305,E501,I201,I101,I100,D204,D101 +max-line-length = 160