How to generate an OpenAPI document with Flask
OpenAPI is a tool for defining and sharing REST APIs, and Flask can be paired with flask-smorest to build such APIs.
This guide walks you through generating an OpenAPI document from a Flask project and using it to create SDKs with Speakeasy, covering the following steps:
- Setting up a simple REST API with Flask
- Integrating
flask-smorest - Creating the OpenAPI document to describe the API
- Customizing the OpenAPI schema
- Using the Speakeasy CLI to create an SDK based on the schema
- Integrating SDK creation into CI/CD workflows
Requirements
To follow along, you will need:
- Python version 3.8 or higher
- An existing Flask project or a copy of the provided example repository
- A basic understanding of Flask project structure and how REST APIs work
Example Flask REST API repository
Example repository
The source code for the completed example is available in the Speakeasy Flask example repository.
The repository already contains all the code covered throughout the guide. You can clone it and follow along with the tutorial, or use it as a reference to add to your own Flask project.
To better understand the process of generating an OpenAPI document with Flask, let’s start by inspecting some simple CRUD endpoints for an online library, along with a Book class and a serializer for our data.
Models and routes
Apps
Open the app.py file, which serves as the main entry point of the program, and inspect the main function:
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources import blp as BookBlueprint
import yaml
app = Flask(__name__)
app.config["API_TITLE"] = "Library API"
app.config["API_VERSION"] = "v0.0.1"
app.config["OPENAPI_VERSION"] = "3.1.0"
app.config["OPENAPI_DESCRIPTION"] = "A simple library API"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database-file.db"
app.config["API_SPEC_OPTIONS"] = {"x-speakeasy-retries": {
'strategy': 'backoff',
'backoff': {
'initialInterval': 500,
'maxInterval': 60000,
'maxElapsedTime': 3600000,
'exponent': 1.5,
},
'statusCodes': ['5XX'],
'retryConnectionErrors': True,
}
}
db.init_app(app)
api = Api(app)
api.register_blueprint(BookBlueprint)
# Add server information to the OpenAPI spec
api.spec.options["servers"] = [
{
"url": "http://127.0.0.1:5000",
"description": "Local development server"
}
]
# Serve OpenAPI spec document endpoint for download
@app.route("/openapi.yaml")
def openapi_yaml():
spec = api.spec.to_dict()
return app.response_class(
yaml.dump(spec, default_flow_style=False),
mimetype="application/x-yaml"
)
if __name__ == "__main__":
with app.app_context():
db.create_all() # Create database tables
app.run(debug=True)Database
Here, you will see a method call to create a SQLite database and a function to run the Flask app:
if __name__ == "__main__":
with app.app_context():
db.create_all() # Create database tables
app.run(debug=True)Models
From the root of the repository, open the models.py file to see a Book model containing a few fields with validation:
from db import db
class Book(db.Model):
__tablename__ = "books"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80), nullable=False)
author = db.Column(db.String(80), nullable=False)
description = db.Column(db.String(200))Schemas
In the schemas.py file, the BookSchema class can be used to serialize and deserialize book data with the marshmallow package:
from marshmallow import Schema, fields
class BookSchema(Schema):
id = fields.Int(dump_only=True)
title = fields.Str(required=True)
author = fields.Str(required=True)
description = fields.Str()Resources
The resources.py file contains API endpoints set up to handle all the CRUD operations for the books:
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import IntegrityError
from db import db
from models import Book
from schemas import BookSchema
blp = Blueprint("Books", "books", url_prefix="/books", description="Operations on books")
@blp.route("/")
class BookList(MethodView):
@blp.response(200, BookSchema(many=True))
@blp.paginate()
def get(self, pagination_parameters):
"""List all books"""
query = Book.query
paginated_books = query.paginate(
page=pagination_parameters.page,
per_page=pagination_parameters.page_size,
error_out=False
)
pagination_parameters.item_count = paginated_books.total
return paginated_books.items
@blp.arguments(BookSchema)
@blp.response(201, BookSchema)
def post(self, new_data):
"""Create a new book"""
book = Book(**new_data)
db.session.add(book)
db.session.commit()
return book
@blp.route("/<int:book_id>")
class BookDetail(MethodView):
@blp.response(200, BookSchema)
def get(self, book_id):
"""Return books based on ID.
---
Internal comment not meant to be exposed.
"""
book = Book.query.get_or_404(book_id)
return book
@blp.arguments(BookSchema)
@blp.response(200, BookSchema)
def put(self, updated_data, book_id):
"""Update an existing book"""
book = Book.query.get_or_404(book_id)
book.title = updated_data["title"]
book.author = updated_data["author"]
book.description = updated_data.get("description")
db.session.commit()
return book
def delete(self, book_id):
"""Delete a book"""
book = Book.query.get_or_404(book_id)
db.session.delete(book)
db.session.commit()
return {"message": "Book deleted"}, 204This code defines a simple Flask REST API with CRUD operations for a Book model. The BookList class provides a way to retrieve all book data and create new books. The BookDetail class handles the retrieval of specific books, updating book data, and deleting books.
Generate the OpenAPI document using flask-smorest
Flask does not support OpenAPI document generation out-of-the-box, so we’ll use the flask-smorest package to generate the OpenAPI document.
If you are following along with the example repository, you can create and activate a virtual environment to install the project dependencies:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txtIf you have not already, install flask-smorest with the following command:
pip install flask-smorestConfiguration
The most basic configuration for generating an OpenAPI document with flask-smorest is added in the app.py file:
app.config["API_TITLE"] = "Library API"
app.config["API_VERSION"] = "v0.0.1"
app.config["OPENAPI_VERSION"] = "3.1.0"
app.config["OPENAPI_DESCRIPTION"] = "A simple library API"SwaggerUI
The new Swagger UI endpoint is also added in the app.py file:
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"Server
The app.py file contains additional configuration settings for the OpenAPI document. These add a development server:
# Add server information to the OpenAPI spec
api.spec.options["servers"] = [
{
"url": "http://127.0.0.1:5000",
"description": "Local development server"
}
]Routes
These additional configuration settings add a route to serve the OpenAPI document:
# Serve OpenAPI spec document endpoint for download
@app.route("/openapi.yaml")
def openapi_yaml():
spec = api.spec.to_dict()
return app.response_class(
yaml.dump(spec, default_flow_style=False),
mimetype="application/x-yaml"
)Run server
To inspect and interact with the OpenAPI document, you need to run the development server, which will create a database file if one does not already exist, and serve the API.
Run the development server:
python app.pyYou can now access the API and documentation:
- Visit
http://127.0.0.1:5000/swagger-uito view the Swagger documentation and interact with the API. - Visit
http://127.0.0.1:5000/openapi.yamlto download the OpenAPI document.
OpenAPI document generation
Now that we understand our Flask REST API, we can run the following command to generate the OpenAPI document using flask-smorest:
flask openapi write --format=yaml openapi.yamlThis generates a new file, openapi.yaml, in the root of the project.
Document
Here, you can see an example of the generated OpenAPI document:
components:
responses:
DEFAULT_ERROR:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: Default error response
UNPROCESSABLE_ENTITY:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: Unprocessable Entity
schemas:
Book:
properties:
author:
type: string
description:
type: string
id:
readOnly: true
type: integer
title:
type: string
required:
- author
- title
type: object
Error:
properties:
code:
description: Error code
type: integer
errors:
additionalProperties: {}
description: Errors
type: object
message:
description: Error message
type: string
status:
description: Error name
type: string
type: object
PaginationMetadata:
properties:
first_page:
type: integer
last_page:
type: integer
next_page:
type: integer
page:
type: integer
previous_page:
type: integer
total:
type: integer
total_pages:
type: integer
type: object
info:
title: Library API
version: v0.0.1
openapi: 3.1.0
paths:
/books/:
get:
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Book'
type: array
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: List all books
tags:
- Books
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
required: true
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
description: Created
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Create a new book
tags:
- Books
/books/{book_id}:
delete:
responses:
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Delete a book
tags:
- Books
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Return books based on ID.
tags:
- Books
parameters:
- in: path
name: book_id
required: true
schema:
minimum: 0
type: integer
put:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
description: OK
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Update an existing book
tags:
- Books
servers:
- description: Local development server
url: http://127.0.0.1:5000
tags:
- description: Operations on books
name: Books
x-speakeasy-retries:
backoff:
exponent: 1.5
initialInterval: 500
maxElapsedTime: 3600000
maxInterval: 60000
retryConnectionErrors: true
statusCodes:
- 5XX
strategy: backoffConfig
Return to the app.py file to see how the app configuration influences the OpenAPI document generation:
app.config["API_TITLE"] = "Library API"
app.config["API_VERSION"] = "v0.0.1"
app.config["OPENAPI_VERSION"] = "3.1.0"
app.config["OPENAPI_DESCRIPTION"] = "A simple library API"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"Metadata
Open the openapi.yaml file to see the titles and versions reflected in the generated OpenAPI document:
info:
title: Library API
version: v0.0.1
openapi: 3.1.0ServerInfo
The server URL is also included in the OpenAPI document:
servers:
- description: Local development server
url: http://127.0.0.1:5000BookParams
Open the models.py file to see the Book parameters:
class Book(db.Model):
__tablename__ = "books"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80), nullable=False)
author = db.Column(db.String(80), nullable=False)
description = db.Column(db.String(200))SchemaRef
Open the openapi.yamlfile to check the same Book parameters are reflected in the OpenAPI document:
schemas:
Book:
properties:
author:
type: string
description:
type: string
id:
readOnly: true
type: integer
title:
type: string
required:
- author
- title
type: objectOpenAPI document customization
The OpenAPI document generated by flask-smorest may not fit all use cases. The document can be customized further to better serve information about your API endpoints. You can add descriptions, tags, examples, and more to make the documentation more informative and user-friendly.
In the customized
Endpoints
Open the resources.py file and inspect the configured endpoints:
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import IntegrityError
from db import db
from models import Book
from schemas import BookSchema
blp = Blueprint("Books", "books", url_prefix="/books", description="Operations on books")
@blp.route("/")
class BookList(MethodView):
@blp.response(200, BookSchema(many=True))
@blp.paginate()
def get(self, pagination_parameters):
"""List all books"""
query = Book.query
paginated_books = query.paginate(
page=pagination_parameters.page,
per_page=pagination_parameters.page_size,
error_out=False
)
pagination_parameters.item_count = paginated_books.total
return paginated_books.items
@blp.arguments(BookSchema)
@blp.response(201, BookSchema)
def post(self, new_data):
"""Create a new book"""
book = Book(**new_data)
db.session.add(book)
db.session.commit()
return book
@blp.route("/<int:book_id>")
class BookDetail(MethodView):
@blp.response(200, BookSchema)
def get(self, book_id):
"""Return books based on ID.
---
Internal comment not meant to be exposed.
"""
book = Book.query.get_or_404(book_id)
return book
@blp.arguments(BookSchema)
@blp.response(200, BookSchema)
def put(self, updated_data, book_id):
"""Update an existing book"""
book = Book.query.get_or_404(book_id)
book.title = updated_data["title"]
book.author = updated_data["author"]
book.description = updated_data.get("description")
db.session.commit()
return book
def delete(self, book_id):
"""Delete a book"""
book = Book.query.get_or_404(book_id)
db.session.delete(book)
db.session.commit()
return {"message": "Book deleted"}, 204Responses
You can indicate the expected response codes and models using @blp.response():
@blp.response(200, BookSchema(many=True))OpenAPIResponse
This results in the following additions, for example, to the /books/ get operation in the OpenAPI document:
/books/:
get:
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Book'
type: array
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: List all books
tags:
- BooksArguments
Use the @blp.arguments() decorator to enforce a schema for arguments:
@blp.arguments(BookSchema)OpenAPIArguments
An enforced arguments schema results in the following additions to the post operation:
requestBody:
content:Pagination
Allow pagination with the @blp.paginate() decorator:
@blp.paginate()PaginationQuery
Allowing paginations gives you access to the page and page_size properties, which you can use in your database query:
def get(self, pagination_parameters):
"""List all books"""
query = Book.query
paginated_books = query.paginate(
page=pagination_parameters.page,
per_page=pagination_parameters.page_size,
error_out=False
)
pagination_parameters.item_count = paginated_books.total
return paginated_books.itemsDocstrings
You can add inline documentation using docstrings:
def get(self, book_id):
"""Return books based on ID.
---
Internal comment not meant to be exposed.
"""DocReflection
Docstrings are reflected in the OpenAPI document as follows:
summary: Return books based on ID.Comments
Notice the internal comment that is omitted from the OpenAPI document:
Internal comment not meant to be exposed.Retries
You can add global retries to the OpenAPI document by modifying the app config in the app.py file:
app.config["API_SPEC_OPTIONS"] = {"x-speakeasy-retries": {
'strategy': 'backoff',
'backoff': {
'initialInterval': 500,
'maxInterval': 60000,
'maxElapsedTime': 3600000,
'exponent': 1.5,
},
'statusCodes': ['5XX'],
'retryConnectionErrors': True,
}
}SDKRetries
This enables retries when using the document to create an SDK with Speakeasy:
x-speakeasy-retries:
backoff:
exponent: 1.5
initialInterval: 500
maxElapsedTime: 3600000
maxInterval: 60000
retryConnectionErrors: true
statusCodes:
- 5XX
strategy: backoffCreating SDKs for a Flask REST API
To create a Python SDK for the Flask REST API, run the following command:
speakeasy quickstartFollow the onscreen prompts to provide the configuration details for your new SDK, such as the name, schema location, and output path. When prompted, enter openapi.yaml for the OpenAPI document location, select a language, and generate.
Add SDK generation to your GitHub Actions
The Speakeasy sdk-generation-action repository provides workflows for integrating the Speakeasy CLI into your CI/CD pipeline, so that your SDKs are recreated whenever your OpenAPI document changes.
You can set up Speakeasy to automatically push a new branch to your SDK repositories for your engineers to review before merging the SDK changes.
For an overview of how to set up automation for your SDKs, see the Speakeasy SDK Generation Action and Workflows documentation.
SDK customization
After creating your SDK with Speakeasy, you will find a new directory containing the generated SDK code, which we will now explore further.
These examples assume a Python SDK named books-python was generated from the example Flask project above. Edit any paths to reflect your environment if you want to follow in your own project.
BookClass
Navigate into the books-python/src/books/models directory and find the book.py file created by Speakeasy. Note how the OpenAPI document was used to create the Book class:
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
from __future__ import annotations
from books.types import BaseModel
from typing import Optional
from typing_extensions import NotRequired, TypedDict
class BookTypedDict(TypedDict):
author: str
title: str
description: NotRequired[str]
id: NotRequired[int]
class Book(BaseModel):
author: str
title: str
description: Optional[str] = None
id: Optional[int] = NoneSDKMethods
Open the src/books/books_sdk.py file to see the methods that call the web API from an application using the SDK:
class BooksSDK(BaseSDK):
r"""Operations on books"""
def get_books_(
self,
*,
page: Optional[int] = 1,
page_size: Optional[int] = 10,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
) -> models.GetBooksResponse:
r"""List all books
:param page:
:param page_size:
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
"""
base_url = None
url_variables = None
if timeout_ms is None:
timeout_ms = self.sdk_configuration.timeout_ms
if server_url is not None:
base_url = server_url
request = models.GetBooksRequest(
page=page,
page_size=page_size,
)
req = self.build_request(
method="GET",
path="/books/",
base_url=base_url,
url_variables=url_variables,
request=request,
request_body_required=False,
request_has_path_params=False,
request_has_query_params=True,
user_agent_header="user-agent",
accept_header_value="application/json",
timeout_ms=timeout_ms,
)
if retries == UNSET:
if self.sdk_configuration.retry_config is not UNSET:
retries = self.sdk_configuration.retry_config
else:
retries = utils.RetryConfig(
"backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True
)
retry_config = None
if isinstance(retries, utils.RetryConfig):
retry_config = (retries, ["5XX"])
http_res = self.do_request(
hook_ctx=HookContext(
operation_id="get_/books/", oauth2_scopes=[], security_source=None
),
request=req,
error_status_codes=["422", "4XX", "5XX"],
retry_config=retry_config,
)
data: Any = None
if utils.match_response(http_res, "200", "application/json"):
return models.GetBooksResponse(
result=utils.unmarshal_json(http_res.text, List[models.Book]),
headers=utils.get_response_headers(http_res.headers),
)
if utils.match_response(http_res, "422", "application/json"):
data = utils.unmarshal_json(http_res.text, models.Error1Data)
raise models.Error1(data=data)
if utils.match_response(http_res, ["4XX", "5XX"], "*"):
http_res_text = utils.stream_to_text(http_res)
raise models.APIError(
"API error occurred", http_res.status_code, http_res_text, http_res
)
if utils.match_response(http_res, "default", "application/json"):
return models.GetBooksResponse(
result=utils.unmarshal_json(http_res.text, models.Error), headers={}
)
content_type = http_res.headers.get("Content-Type")
http_res_text = utils.stream_to_text(http_res)
raise models.APIError(
f"Unexpected response received (code: {http_res.status_code}, type: {content_type})",
http_res.status_code,
http_res_text,
http_res,
)Requests
Here, you can see how the request to the API endpoint is built:
req = self.build_request(
method="GET",
path="/books/",
base_url=base_url,
url_variables=url_variables,
request=request,
request_body_required=False,
request_has_path_params=False,
request_has_query_params=True,
user_agent_header="user-agent",
accept_header_value="application/json",
timeout_ms=timeout_ms,
)RetriesConfig
Finally, note the result of the global retries strategy that we set up in the app.py file:
retries = utils.RetryConfig(
"backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True
)Summary
In this guide, we showed you how to generate an OpenAPI document for a Flask API and use Speakeasy to create an SDK based on the OpenAPI document. The step-by-step instructions included adding relevant tools to the Flask project, generating an OpenAPI document, enhancing it for improved creation, using Speakeasy CLI to create the SDKs, and interpreting the basics of the generated SDK.
We also explored automating SDK generation through CI/CD workflows and improving API operations.
Last updated on