# 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:

```bash filename="Terminal"
python app.py
```

You can now access the API and documentation:

- Visit `http://127.0.0.1:5000/swagger-ui` to view the Swagger documentation and interact with the API.
- Visit `http://127.0.0.1:5000/openapi.yaml` to 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`:

```bash filename="Terminal"
flask openapi write --format=yaml openapi.yaml
```

This generates a new file, `openapi.yaml`, in the root of the project.



### Document

Here, you can see an example of the generated OpenAPI document:

```yaml
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: backoff
```

### Config

Return to the `app.py` file to see how the app configuration influences the OpenAPI document generation:

```python
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:

```yaml
info:
  title: Library API
  version: v0.0.1
openapi: 3.1.0
```

### ServerInfo

The server URL is also included in the OpenAPI document:

```yaml
servers:
- description: Local development server
  url: http://127.0.0.1:5000
```

### BookParams

Open the `models.py` file to see the `Book` parameters:

```python
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.yaml`file to check the same `Book` parameters are reflected in the OpenAPI document:

```yaml
  schemas:
    Book:
      properties:
        author:
          type: string
        description:
          type: string
        id:
          readOnly: true
          type: integer
        title:
          type: string
      required:
      - author
      - title
      type: object
```



## OpenAPI 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](https://github.com/speakeasy-api/openapi-flask-example/tree/customized) branch of the example repository, you can find a customized OpenAPI document that demonstrates the options available for modifying your generated document.



### Endpoints

Open the `resources.py` file and inspect the configured endpoints:

```python
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"}, 204
```

### Responses

You can indicate the expected response codes and models using `@blp.response()`:

```python
    @blp.response(200, BookSchema(many=True))
```

### OpenAPIResponse

This results in the following additions, for example, to the `/books/` `get` operation in the OpenAPI document:

```yaml
  /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
```

### Arguments

Use the `@blp.arguments()` decorator to enforce a schema for arguments:

```python
    @blp.arguments(BookSchema)
```

### OpenAPIArguments

An enforced arguments schema results in the following additions to the `post` operation:

```yaml
      requestBody:
        content:
```

### Pagination

Allow pagination with the `@blp.paginate()` decorator:

```python
    @blp.paginate()
```

### PaginationQuery

Allowing paginations gives you access to the `page` and `page_size` properties, which you can use in your database query:

```python
    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
```

### Docstrings

You can add inline documentation using docstrings:

```python
    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:

```yaml
      summary: Return books based on ID.
```

### Comments

Notice the internal comment that is omitted from the OpenAPI document:

```python
        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:

```python
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:

```yaml
x-speakeasy-retries:
  backoff:
    exponent: 1.5
    initialInterval: 500
    maxElapsedTime: 3600000
    maxInterval: 60000
  retryConnectionErrors: true
  statusCodes:
  - 5XX
  strategy: backoff
```



## Creating SDKs for a Flask REST API

To create a Python SDK for the Flask REST API, run the following command:

```bash filename="Terminal"
speakeasy quickstart
```

Follow 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`](https://github.com/speakeasy-api/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](/docs/speakeasy-reference/workflow-file) 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:

```python
"""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] = None
```

### SDKMethods

Open the `src/books/books_sdk.py` file to see the methods that call the web API from an application using the SDK:

```python mark=13
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:

```python
        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:

```python
                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.