# Use extend_schema_view to customize the entire viewset
@extend_schema_view(
    list=extend_schema(
        summary="List all books",
        description="Get a list of all books in the library.",
        responses={
            200: BookSerializer(many=True)
        },
        tags=["books"],
    ),
    retrieve=extend_schema(
        summary="Get a specific book",
        description="Retrieve details for a specific book by its ID.",
        responses={
            200: BookSerializer,
            404: OpenApiResponse(description="Book not found"),
        },
        tags=["books"],
    ),
    # Other methods...
)
class BookViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows books to be viewed or edited.
    """
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    # Use extend_schema to customize a specific action
    @extend_schema(
        summary="Find books by the same author",
        description="Returns all books written by the same author as the specified book.",
        responses={
            200: BookSerializer(many=True),
            404: OpenApiResponse(
                description="Book not found",
                examples=[
                    OpenApiExample(
                        "Error Response",
                        value={"error": "Book not found"},
                        status_codes=["404"],
                    )
                ]
            )
        },
        tags=["books", "authors"],
        parameters=[
            OpenApiParameter(
                name="sort",
                description="Sort order for the books",
                required=False,
                type=str,
                enum=["title", "published_year"],
            ),
        ],
        examples=[
            OpenApiExample(
                "Book list example",
                value=[
                    {
                        "id": 1,
                        "title": "The Great Gatsby",
                        "author": "F. Scott Fitzgerald",
                        "published_year": 1925
                    },
                    {
                        "id": 2,
                        "title": "Tender Is the Night",
                        "author": "F. Scott Fitzgerald",
                        "published_year": 1934
                    }
                ],
                response_only=True,
                status_codes=["200"],
            )
        ],
        extensions={
            "x-speakeasy-retries": {
                "strategy": "backoff",
                "backoff": {
                    "initialInterval": 500,
                    "maxInterval": 60000,
                    "maxElapsedTime": 3600000,
                    "exponent": 1.5,
                },
                "statusCodes": ["5XX"],
                "retryConnectionErrors": True,
            }
        }
    )
    @action(detail=True, methods=['get'])
    def author_books(self, request, pk=None):
        # Implementation details...
        pass
```

#### Using @extend_schema_view

The `@extend_schema_view` decorator allows you to customize all methods in a viewset at once. In our example, we're customizing the `list` and `retrieve` operations with summaries, descriptions, and response details.

This will appear in the generated OpenAPI document as:

```yaml filename="openapi.yaml"
paths:
  /api/books/:
    get:
      operationId: books_list
      summary: List all books
      description: Get a list of all books in the library.
      parameters:
        # Standard parameters here
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Book'
      tags:
        - books
```

#### Customizing Individual Actions with @extend_schema

For specific actions like `author_books`, we use the `@extend_schema` decorator to add detailed documentation:

```python filename="views.py"
@extend_schema(
    summary="Find books by the same author",
    description="Returns all books written by the same author as the specified book.",
    # Other options...
)
@action(detail=True, methods=['get'])
def author_books(self, request, pk=None):
    # Implementation...
```

This will generate OpenAPI documentation for this endpoint:

```yaml filename="openapi.yaml"
/api/books/{id}/author_books/:
  get:
    operationId: books_author_books
    summary: Find books by the same author
    description: Returns all books written by the same author as the specified book.
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
      - name: sort
        in: query
        description: Sort order for the books
        required: false
        schema:
          type: string
          enum:
            - title
            - published_year
    responses:
      '200':
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/Book'
            examples:
              Book list example:
                value:
                  - id: 1
                    title: The Great Gatsby
                    author: F. Scott Fitzgerald
                    published_year: 1925
                  - id: 2
                    title: Tender Is the Night
                    author: F. Scott Fitzgerald
                    published_year: 1934
      '404':
        description: Book not found
        content:
          application/json:
            examples:
              Error Response:
                value:
                  error: Book not found
    tags:
      - books
      - authors
```

#### Adding Custom Parameters

You can add custom query parameters to your endpoints using `OpenApiParameter`:

```python filename="views.py"
parameters=[
    OpenApiParameter(
        name="sort",
        description="Sort order for the books",
        required=False,
        type=str,
        enum=["title", "published_year"],
    ),
]
```

#### Adding Examples

Examples help API users understand the expected responses:

```python filename="views.py"
examples=[
    OpenApiExample(
        "Book list example",
        value=[
            {
                "id": 1,
                "title": "The Great Gatsby",
                "author": "F. Scott Fitzgerald",
                "published_year": 1925
            },
            # More examples...
        ],
        response_only=True,
        status_codes=["200"],
    )
]
```

#### Adding Retry Logic

You can add retry configuration at the global level in `settings.py`:

```python filename="settings.py"
'EXTENSIONS_TO_SCHEMA_FUNCTION': lambda generator, request, public: {
    'x-speakeasy-retries': {
        'strategy': 'backoff',
        'backoff': {
            'initialInterval': 500,
            'maxInterval': 60000,
            'maxElapsedTime': 3600000,
            'exponent': 1.5,
        },
        'statusCodes': ['5XX'],
        'retryConnectionErrors': True,
    }
}
```

Or apply it to specific endpoints using the `extensions` parameter in `@extend_schema`:

```python filename="views.py"   
extensions={
    "x-speakeasy-retries": {
        "strategy": "backoff",
        "backoff": {
            "initialInterval": 500,
            "maxInterval": 60000,
            "maxElapsedTime": 3600000,
            "exponent": 1.5,
        },
        "statusCodes": ["5XX"],
        "retryConnectionErrors": True,
    }
}
```

In summary, the `drf-spectacular` package provides a variety of ways to customize the OpenAPI document for your Django REST API. You can use decorators, tags, descriptions, parameters, fields, examples, and global settings to modify the document according to your requirements.

- **Decorators (@extend_schema and @extend_schema_view):** Customize individual methods or entire views.
- **Tags and descriptions:** Organize endpoints for better readability.
- **Parameters:** Define custom parameters using `OpenApiParameter`.
- **OpenAPI components:** Use `OpenApiExample` to provide reusable components or examples.
- **Global settings (`SPECTACULAR_SETTINGS`):** Modify the global behavior of `drf-spectacular`.

For more information about customizing the OpenAPI schema with `drf-spectacular`, refer to the official [`drf-spectacular` documentation](https://drf-spectacular.readthedocs.io/en/latest/).



## Creating SDKs for a Django REST API

To create a Python SDK for the Django 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

Explore the effects of your newly generated OpenAPI document on the SDK created by Speakeasy.

After creating your SDK with Speakeasy, you will find a new directory containing the generated SDK code. Let's explore this code a bit further.

These examples assume a Python SDK named `books-python` was generated from the example Django project above. Edit any paths to reflect your environment if you want to follow in your own project.

### Exploring the Generated SDK

After generating your SDK with Speakeasy, let's explore the key files and how they relate to your OpenAPI document.

#### The Book Class

Navigate to the `books-python/src/books` directory to find the generated SDK code. The `book.py` file contains the `Book` class that corresponds to your Django model:

```python filename="book.py" 
from __future__ import annotations
import dateutil.parser
from datetime import datetime
from marshmallow import fields
from typing import Any, Dict, List, Optional, Union
from dataclasses import dataclass
from dataclasses_json import dataclass_json
from books import utils


@dataclass_json
@dataclass
class Book:
    """A book in the library API"""
    
    id: Optional[int] = None
    title: str = None
    author: str = None
    published_year: int = None
    
    def unmarshal(
        obj: Union[Dict[str, Any], str]
    ) -> Book:
        """
        Unmarshals a Book from a dictionary or a JSON string.
        """
        if isinstance(obj, str):
            obj = utils.loads(obj)
            
        return Book(
            id=obj.get("id"),
            title=obj.get("title"),
            author=obj.get("author"),
            published_year=obj.get("published_year"),
        )
```

#### API Client Code

The `api.py` file contains methods that call the web API from an application using the SDK:

```python filename="api.py"
class BooksSDK:
    """SDK for accessing the Library API"""
    
    def __init__(
        self,
        security: Optional[shared.Security] = None,
        retries: Optional[utils.RetryConfig] = None,
        server_url: Optional[str] = None,
        server_idx: Optional[int] = None,
        client_config: Optional[client.ClientConfig] = None,
    ):
        """Initialize the SDK client"""
        
        if server_url is not None:
            self.server_url = server_url
        elif server_idx is not None:
            self.server_url = utils.SERVERS[server_idx]
        else:
            self.server_url = utils.SERVERS[0]
            
        self.client_config = client_config
        self.security = security
        self.retries = retries
```

Notice several important parameters:

1. The `server_url` parameter, which comes from the `SERVERS` key in your `SPECTACULAR_SETTINGS`:

```python filename="api.py"
self.server_url = utils.SERVERS[0]  # Default to first server
```

2. The `retries` parameter, which is generated from your retry configuration:

```python filename="api.py"
self.retries = retries
```

#### Making API Requests

These parameters are used to build requests to your API endpoints:

```python filename="api.py"
def list_books(
    self,
    request_options: Optional[utils.RequestOptions] = None,
) -> operations.BooksListResponse:
    """List all books in the library"""
    
    base_url = self.server_url
    
    url = utils.generate_url(base_url, "/api/books/")
    
    headers = {}
    headers["Accept"] = "application/json"
    
    client = self.get_client()
    
    retry_config = request_options.retry_config if request_options and request_options.retry_config is not None else self.retries
    
    return utils.retry(
        lambda: client.request("GET", url, headers=headers),
        retry_config
    )
```

#### Retry Logic Implementation

The SDK includes a retry implementation based on your OpenAPI extensions:

```python filename="retries.py" 
class RetryConfig:
    """Configuration for retry behavior"""

    def __init__(
        self,
        strategy: str = None,
        backoff: Optional[BackoffStrategy] = None,
        retry_connection_errors: bool = False,
        status_codes: Optional[List[str]] = None,
    ):
        """Initialize retry configuration"""
        
        self.strategy = strategy
        self.backoff = backoff
        self.retry_connection_errors = retry_connection_errors
        self.status_codes = status_codes