quas-docs 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quas_docs/__init__.py +32 -0
- quas_docs/config.py +246 -0
- quas_docs/core.py +483 -0
- quas_docs-0.0.3.dist-info/METADATA +493 -0
- quas_docs-0.0.3.dist-info/RECORD +7 -0
- quas_docs-0.0.3.dist-info/WHEEL +5 -0
- quas_docs-0.0.3.dist-info/top_level.txt +1 -0
quas_docs/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask OpenAPI Documentation Package
|
|
3
|
+
|
|
4
|
+
A reusable package for generating comprehensive OpenAPI documentation
|
|
5
|
+
with Flask-Pydantic-Spec, featuring custom metadata, security schemes,
|
|
6
|
+
and flexible configuration.
|
|
7
|
+
|
|
8
|
+
Author: Emmanuel Olowu
|
|
9
|
+
License: MIT
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .core import FlaskOpenAPISpec, SecurityScheme, QueryParameter
|
|
13
|
+
from .config import DocsConfig, ContactInfo, SecuritySchemeConfig
|
|
14
|
+
|
|
15
|
+
__version__ = "0.0.3"
|
|
16
|
+
__author__ = "Emmanuel Olowu"
|
|
17
|
+
|
|
18
|
+
# Create a default instance for endpoint decorator export
|
|
19
|
+
_default_spec = FlaskOpenAPISpec(DocsConfig.create_default())
|
|
20
|
+
endpoint = _default_spec.endpoint
|
|
21
|
+
|
|
22
|
+
# Public API
|
|
23
|
+
__all__ = [
|
|
24
|
+
"FlaskOpenAPISpec",
|
|
25
|
+
"SecurityScheme",
|
|
26
|
+
"QueryParameter",
|
|
27
|
+
"endpoint",
|
|
28
|
+
"DocsConfig",
|
|
29
|
+
"ContactInfo",
|
|
30
|
+
"SecuritySchemeConfig",
|
|
31
|
+
]
|
|
32
|
+
|
quas_docs/config.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration system for Flask OpenAPI Documentation Package.
|
|
3
|
+
|
|
4
|
+
This module provides a flexible configuration system that allows easy
|
|
5
|
+
customization of API documentation metadata, security schemes, and behavior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict, Any, List, Optional
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ContactInfo:
|
|
16
|
+
"""Contact information for the API documentation."""
|
|
17
|
+
email: Optional[str] = None
|
|
18
|
+
name: Optional[str] = None
|
|
19
|
+
url: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
22
|
+
"""Convert to dictionary for OpenAPI spec."""
|
|
23
|
+
return {k: v for k, v in {
|
|
24
|
+
'email': self.email,
|
|
25
|
+
'name': self.name,
|
|
26
|
+
'url': self.url
|
|
27
|
+
}.items() if v is not None}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SecuritySchemeConfig:
|
|
32
|
+
"""Configuration for a security scheme."""
|
|
33
|
+
name: str
|
|
34
|
+
scheme_type: str = "apiKey"
|
|
35
|
+
location: str = "header" # header, query, cookie
|
|
36
|
+
parameter_name: str = "Authorization"
|
|
37
|
+
description: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
def to_openapi_dict(self) -> Dict[str, Any]:
|
|
40
|
+
"""Convert to OpenAPI security scheme format."""
|
|
41
|
+
scheme = {
|
|
42
|
+
'type': self.scheme_type,
|
|
43
|
+
'in': self.location,
|
|
44
|
+
'name': self.parameter_name,
|
|
45
|
+
}
|
|
46
|
+
if self.description:
|
|
47
|
+
scheme['description'] = self.description
|
|
48
|
+
return scheme
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DocsConfig:
|
|
53
|
+
"""Main configuration class for Flask OpenAPI documentation."""
|
|
54
|
+
|
|
55
|
+
# Basic API Information
|
|
56
|
+
title: str = "Flask API"
|
|
57
|
+
version: str = "0.0.1"
|
|
58
|
+
description: Optional[str] = None
|
|
59
|
+
terms_of_service: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
# Contact Information
|
|
62
|
+
contact: Optional[ContactInfo] = None
|
|
63
|
+
|
|
64
|
+
# License Information
|
|
65
|
+
license_name: Optional[str] = None
|
|
66
|
+
license_url: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
# Server Information
|
|
69
|
+
servers: List[Dict[str, str]] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
# Security Schemes
|
|
72
|
+
security_schemes: Dict[str, SecuritySchemeConfig] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
# Customization Options
|
|
75
|
+
preserve_flask_routes: bool = True # Keep <string:param> format
|
|
76
|
+
clear_auto_discovered: bool = True # Remove auto-discovered duplicates
|
|
77
|
+
add_default_responses: bool = True # Add default response schemas
|
|
78
|
+
|
|
79
|
+
# External Documentation
|
|
80
|
+
external_docs_url: Optional[str] = None
|
|
81
|
+
external_docs_description: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def create_default(cls) -> 'DocsConfig':
|
|
85
|
+
"""Create a default configuration with common settings."""
|
|
86
|
+
return cls(
|
|
87
|
+
title="Flask API",
|
|
88
|
+
version="0.0.1",
|
|
89
|
+
description="API documentation generated with Flask OpenAPI Docs",
|
|
90
|
+
contact=ContactInfo(
|
|
91
|
+
email="api@example.com",
|
|
92
|
+
name="API Team"
|
|
93
|
+
),
|
|
94
|
+
security_schemes={
|
|
95
|
+
"BearerAuth": SecuritySchemeConfig(
|
|
96
|
+
name="BearerAuth",
|
|
97
|
+
description="JWT Bearer token authentication"
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_env(cls, prefix: str = "API_") -> 'DocsConfig':
|
|
104
|
+
"""Create configuration from environment variables with optional prefix.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
prefix: Environment variable prefix (default: 'API_')
|
|
108
|
+
|
|
109
|
+
Environment Variables:
|
|
110
|
+
{prefix}TITLE: API title
|
|
111
|
+
{prefix}VERSION: API version
|
|
112
|
+
{prefix}DESCRIPTION: API description
|
|
113
|
+
{prefix}CONTACT_EMAIL: Contact email
|
|
114
|
+
{prefix}CONTACT_NAME: Contact name
|
|
115
|
+
{prefix}CONTACT_URL: Contact URL
|
|
116
|
+
{prefix}LICENSE_NAME: License name
|
|
117
|
+
{prefix}LICENSE_URL: License URL
|
|
118
|
+
{prefix}PRESERVE_FLASK_ROUTES: Keep Flask route format (true/false)
|
|
119
|
+
"""
|
|
120
|
+
import os
|
|
121
|
+
|
|
122
|
+
# Helper function to get bool from env
|
|
123
|
+
def get_bool(key: str, default: bool) -> bool:
|
|
124
|
+
value = os.getenv(key, str(default)).lower()
|
|
125
|
+
return value in ('true', '1', 'yes', 'on')
|
|
126
|
+
|
|
127
|
+
# Create contact info if any contact env vars are present
|
|
128
|
+
contact = None
|
|
129
|
+
contact_email = os.getenv(f"{prefix}CONTACT_EMAIL")
|
|
130
|
+
contact_name = os.getenv(f"{prefix}CONTACT_NAME")
|
|
131
|
+
contact_url = os.getenv(f"{prefix}CONTACT_URL")
|
|
132
|
+
|
|
133
|
+
if any([contact_email, contact_name, contact_url]):
|
|
134
|
+
contact = ContactInfo(
|
|
135
|
+
email=contact_email,
|
|
136
|
+
name=contact_name,
|
|
137
|
+
url=contact_url
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return cls(
|
|
141
|
+
title=os.getenv(f"{prefix}TITLE", "Flask API"),
|
|
142
|
+
version=os.getenv(f"{prefix}VERSION", "0.0.1"),
|
|
143
|
+
description=os.getenv(f"{prefix}DESCRIPTION"),
|
|
144
|
+
contact=contact,
|
|
145
|
+
license_name=os.getenv(f"{prefix}LICENSE_NAME"),
|
|
146
|
+
license_url=os.getenv(f"{prefix}LICENSE_URL"),
|
|
147
|
+
preserve_flask_routes=get_bool(f"{prefix}PRESERVE_FLASK_ROUTES", True),
|
|
148
|
+
clear_auto_discovered=get_bool(f"{prefix}CLEAR_AUTO_DISCOVERED", True),
|
|
149
|
+
add_default_responses=get_bool(f"{prefix}ADD_DEFAULT_RESPONSES", True)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_dict(cls, config_dict: Dict[str, Any]) -> 'DocsConfig':
|
|
154
|
+
"""Create configuration from a dictionary.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
config_dict: Dictionary containing configuration values
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
config = DocsConfig.from_dict({
|
|
161
|
+
'title': 'My API',
|
|
162
|
+
'version': '2.0.0',
|
|
163
|
+
'contact': {'email': 'dev@example.com', 'name': 'Dev Team'},
|
|
164
|
+
'security_schemes': {
|
|
165
|
+
'BearerAuth': {'description': 'JWT authentication'}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
"""
|
|
169
|
+
# Extract contact info if present
|
|
170
|
+
contact = None
|
|
171
|
+
if 'contact' in config_dict:
|
|
172
|
+
contact_data = config_dict['contact']
|
|
173
|
+
if isinstance(contact_data, dict):
|
|
174
|
+
contact = ContactInfo(**contact_data)
|
|
175
|
+
|
|
176
|
+
# Extract security schemes if present
|
|
177
|
+
security_schemes = {}
|
|
178
|
+
if 'security_schemes' in config_dict:
|
|
179
|
+
schemes_data = config_dict['security_schemes']
|
|
180
|
+
if isinstance(schemes_data, dict):
|
|
181
|
+
for name, scheme_data in schemes_data.items():
|
|
182
|
+
if isinstance(scheme_data, dict):
|
|
183
|
+
security_schemes[name] = SecuritySchemeConfig(name=name, **scheme_data)
|
|
184
|
+
|
|
185
|
+
# Create the config with known fields
|
|
186
|
+
known_fields = {
|
|
187
|
+
'title', 'version', 'description', 'terms_of_service',
|
|
188
|
+
'license_name', 'license_url', 'servers', 'preserve_flask_routes',
|
|
189
|
+
'clear_auto_discovered', 'add_default_responses',
|
|
190
|
+
'external_docs_url', 'external_docs_description'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
filtered_dict = {k: v for k, v in config_dict.items() if k in known_fields}
|
|
194
|
+
|
|
195
|
+
return cls(
|
|
196
|
+
contact=contact,
|
|
197
|
+
security_schemes=security_schemes,
|
|
198
|
+
**filtered_dict
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def add_security_scheme(self, name: str, config: SecuritySchemeConfig) -> None:
|
|
202
|
+
"""Add a new security scheme to the configuration."""
|
|
203
|
+
self.security_schemes[name] = config
|
|
204
|
+
|
|
205
|
+
def add_server(self, url: str, description: str = "") -> None:
|
|
206
|
+
"""Add a server to the configuration."""
|
|
207
|
+
self.servers.append({"url": url, "description": description})
|
|
208
|
+
|
|
209
|
+
def to_openapi_info(self) -> Dict[str, Any]:
|
|
210
|
+
"""Convert to OpenAPI info object."""
|
|
211
|
+
info: Dict[str, Any] = {
|
|
212
|
+
"title": self.title,
|
|
213
|
+
"version": self.version
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if self.description:
|
|
217
|
+
info["description"] = self.description
|
|
218
|
+
if self.terms_of_service:
|
|
219
|
+
info["termsOfService"] = self.terms_of_service
|
|
220
|
+
if self.contact:
|
|
221
|
+
contact_dict = self.contact.to_dict()
|
|
222
|
+
if contact_dict:
|
|
223
|
+
info["contact"] = contact_dict
|
|
224
|
+
if self.license_name:
|
|
225
|
+
license_info: Dict[str, Any] = {"name": self.license_name}
|
|
226
|
+
if self.license_url:
|
|
227
|
+
license_info["url"] = self.license_url
|
|
228
|
+
info["license"] = license_info
|
|
229
|
+
|
|
230
|
+
return info
|
|
231
|
+
|
|
232
|
+
def to_openapi_security_schemes(self) -> Dict[str, Dict[str, Any]]:
|
|
233
|
+
"""Convert security schemes to OpenAPI format."""
|
|
234
|
+
return {
|
|
235
|
+
name: config.to_openapi_dict()
|
|
236
|
+
for name, config in self.security_schemes.items()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def to_openapi_external_docs(self) -> Optional[Dict[str, Any]]:
|
|
240
|
+
"""Convert external docs to OpenAPI format."""
|
|
241
|
+
if self.external_docs_url:
|
|
242
|
+
docs: Dict[str, Any] = {"url": self.external_docs_url}
|
|
243
|
+
if self.external_docs_description:
|
|
244
|
+
docs["description"] = self.external_docs_description
|
|
245
|
+
return docs
|
|
246
|
+
return None
|
quas_docs/core.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core functionality for Flask OpenAPI Documentation Package.
|
|
3
|
+
|
|
4
|
+
This module contains the main FlaskOpenAPISpec class and supporting utilities
|
|
5
|
+
for generating comprehensive OpenAPI documentation with custom metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
import re
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Tuple
|
|
14
|
+
|
|
15
|
+
from flask import Flask
|
|
16
|
+
from flask_pydantic_spec import FlaskPydanticSpec
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from .config import DocsConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SecurityScheme(str, Enum):
|
|
23
|
+
"""Available security schemes for API endpoints."""
|
|
24
|
+
PUBLIC_BEARER = "PublicBearerAuth"
|
|
25
|
+
ADMIN_BEARER = "AdminBearerAuth"
|
|
26
|
+
BEARER_AUTH = "BearerAuth"
|
|
27
|
+
NONE = "none"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class QueryParameter:
|
|
31
|
+
"""Represents a query parameter definition."""
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
name: str,
|
|
35
|
+
type_: str = "string",
|
|
36
|
+
required: bool = False,
|
|
37
|
+
description: Optional[str] = None,
|
|
38
|
+
default: Any = None
|
|
39
|
+
):
|
|
40
|
+
self.name = name
|
|
41
|
+
self.type_ = type_
|
|
42
|
+
self.required = required
|
|
43
|
+
self.description = description or f"The {name} parameter"
|
|
44
|
+
self.default = default
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EndpointMetadata:
|
|
48
|
+
"""Container for all endpoint metadata including request body, security, etc."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
request_body: Optional[Type[BaseModel]] = None,
|
|
53
|
+
security: Optional[SecurityScheme] = None,
|
|
54
|
+
tags: Optional[List[str]] = None,
|
|
55
|
+
summary: Optional[str] = None,
|
|
56
|
+
description: Optional[str] = None,
|
|
57
|
+
deprecated: bool = False,
|
|
58
|
+
query_params: Optional[List[QueryParameter]] = None,
|
|
59
|
+
responses: Optional[Dict[Any, Any]] = None,
|
|
60
|
+
**extra_metadata: Any
|
|
61
|
+
):
|
|
62
|
+
self.request_body = request_body
|
|
63
|
+
self.security = security
|
|
64
|
+
self.tags = tags or []
|
|
65
|
+
self.summary = summary
|
|
66
|
+
self.description = description
|
|
67
|
+
self.deprecated = deprecated
|
|
68
|
+
self.query_params = query_params or []
|
|
69
|
+
self.responses: Dict[Any, Any] = responses or {}
|
|
70
|
+
self.extra_metadata = extra_metadata
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FlaskOpenAPISpec:
|
|
74
|
+
"""
|
|
75
|
+
Main class for Flask OpenAPI documentation generation.
|
|
76
|
+
|
|
77
|
+
Provides a clean, configurable interface for generating comprehensive
|
|
78
|
+
OpenAPI documentation with custom metadata, security schemes, and
|
|
79
|
+
flexible configuration options.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, config: Optional[DocsConfig] = None):
|
|
83
|
+
"""
|
|
84
|
+
Initialize the OpenAPI spec generator.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Configuration object. If None, creates default config.
|
|
88
|
+
"""
|
|
89
|
+
self.config = config or DocsConfig.create_default()
|
|
90
|
+
self.spec = FlaskPydanticSpec(
|
|
91
|
+
'flask',
|
|
92
|
+
title=self.config.title,
|
|
93
|
+
version=self.config.version
|
|
94
|
+
)
|
|
95
|
+
self._registered_endpoints: List[Tuple[str, str, EndpointMetadata]] = []
|
|
96
|
+
|
|
97
|
+
def init_app(self, app: Flask) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Initialize the OpenAPI documentation for a Flask application.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
app: The Flask application instance to configure
|
|
103
|
+
"""
|
|
104
|
+
# Extract our custom endpoints before registering the spec
|
|
105
|
+
self._extract_decorated_endpoints(app)
|
|
106
|
+
|
|
107
|
+
# Register the spec with the app
|
|
108
|
+
self.spec.register(app)
|
|
109
|
+
|
|
110
|
+
# Access the underlying OpenAPI spec object
|
|
111
|
+
inner: Any = getattr(self.spec, 'spec', None)
|
|
112
|
+
if inner is None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Clear auto-discovered paths to prevent duplicates if configured
|
|
116
|
+
if self.config.clear_auto_discovered and isinstance(inner, dict) and 'paths' in inner:
|
|
117
|
+
inner['paths'] = {}
|
|
118
|
+
|
|
119
|
+
# Apply our customizations after spec registration
|
|
120
|
+
self._setup_security_schemes(inner)
|
|
121
|
+
self._setup_info_metadata(inner)
|
|
122
|
+
self._setup_servers(inner)
|
|
123
|
+
self._setup_external_docs(inner)
|
|
124
|
+
|
|
125
|
+
# Apply our custom endpoint metadata
|
|
126
|
+
self._apply_registered_endpoints(inner)
|
|
127
|
+
|
|
128
|
+
def endpoint(
|
|
129
|
+
self,
|
|
130
|
+
request_body: Optional[Type[BaseModel]] = None,
|
|
131
|
+
security: Optional[SecurityScheme] = None,
|
|
132
|
+
tags: Optional[List[str]] = None,
|
|
133
|
+
summary: Optional[str] = None,
|
|
134
|
+
description: Optional[str] = None,
|
|
135
|
+
deprecated: bool = False,
|
|
136
|
+
query_params: Optional[List[QueryParameter]] = None,
|
|
137
|
+
responses: Optional[Dict[Any, Any]] = None,
|
|
138
|
+
**extra_metadata: Any
|
|
139
|
+
) -> Callable:
|
|
140
|
+
"""
|
|
141
|
+
Unified decorator for comprehensive endpoint documentation.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
request_body: Pydantic model class for request body validation/documentation
|
|
145
|
+
security: Security scheme required for this endpoint
|
|
146
|
+
tags: List of tags for grouping endpoints in documentation
|
|
147
|
+
summary: Brief summary of the endpoint functionality
|
|
148
|
+
description: Detailed description of the endpoint
|
|
149
|
+
deprecated: Whether this endpoint is deprecated
|
|
150
|
+
responses: Mapping of status codes to Pydantic models or OpenAPI schema dicts
|
|
151
|
+
**extra_metadata: Additional metadata for future extensibility
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Decorator function that preserves the original view function behavior
|
|
155
|
+
"""
|
|
156
|
+
def decorator(func: Callable) -> Callable:
|
|
157
|
+
# Create metadata object
|
|
158
|
+
metadata = EndpointMetadata(
|
|
159
|
+
request_body=request_body,
|
|
160
|
+
security=security,
|
|
161
|
+
tags=tags,
|
|
162
|
+
summary=summary,
|
|
163
|
+
description=description,
|
|
164
|
+
deprecated=deprecated,
|
|
165
|
+
query_params=query_params,
|
|
166
|
+
responses=responses,
|
|
167
|
+
**extra_metadata
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Attach metadata to function for later discovery
|
|
171
|
+
func._endpoint_metadata = metadata # type: ignore
|
|
172
|
+
|
|
173
|
+
@functools.wraps(func)
|
|
174
|
+
def wrapper(*args, **kwargs):
|
|
175
|
+
return func(*args, **kwargs)
|
|
176
|
+
|
|
177
|
+
# Ensure metadata is also attached to the wrapper
|
|
178
|
+
wrapper._endpoint_metadata = metadata # type: ignore
|
|
179
|
+
return wrapper
|
|
180
|
+
return decorator
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _extract_decorated_endpoints(self, app: Flask) -> None:
|
|
185
|
+
"""Extract endpoint metadata from decorated view functions."""
|
|
186
|
+
try:
|
|
187
|
+
for rule in app.url_map.iter_rules():
|
|
188
|
+
endpoint = rule.endpoint
|
|
189
|
+
view_func = app.view_functions.get(endpoint)
|
|
190
|
+
|
|
191
|
+
if not view_func:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
metadata = None
|
|
195
|
+
|
|
196
|
+
# Check for endpoint metadata
|
|
197
|
+
if hasattr(view_func, '_endpoint_metadata'):
|
|
198
|
+
metadata = view_func._endpoint_metadata
|
|
199
|
+
|
|
200
|
+
if metadata:
|
|
201
|
+
# Extract valid HTTP methods (exclude HEAD, OPTIONS)
|
|
202
|
+
methods = [m.lower() for m in rule.methods or set()
|
|
203
|
+
if m.lower() not in ('head', 'options')]
|
|
204
|
+
|
|
205
|
+
if not methods:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Use the first valid method for documentation
|
|
209
|
+
method = methods[0]
|
|
210
|
+
|
|
211
|
+
# Handle path format based on configuration
|
|
212
|
+
path = rule.rule
|
|
213
|
+
if not self.config.preserve_flask_routes:
|
|
214
|
+
# Convert Flask route format to OpenAPI format
|
|
215
|
+
# <string:param> -> {param}, <int:param> -> {param}, etc.
|
|
216
|
+
path = re.sub(r'<(?:\w+:)?(\w+)>', r'{\1}', path)
|
|
217
|
+
|
|
218
|
+
# Try to extract response schemas from @spec.validate decorator
|
|
219
|
+
try:
|
|
220
|
+
if hasattr(view_func, '_spec_responses'):
|
|
221
|
+
responses = getattr(view_func, '_spec_responses', {})
|
|
222
|
+
norm_responses = self._normalize_spec_responses(responses)
|
|
223
|
+
if norm_responses:
|
|
224
|
+
metadata.responses.update(norm_responses)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
self._registered_endpoints.append((path, method, metadata))
|
|
229
|
+
except Exception:
|
|
230
|
+
# Silently continue - documentation generation should never break the app
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
def _normalize_spec_responses(self, raw_responses: Any) -> Dict[str, Any]:
|
|
234
|
+
"""Normalize flask-pydantic-spec response mapping into OpenAPI-ready dict."""
|
|
235
|
+
normalized: Dict[str, Any] = {}
|
|
236
|
+
if isinstance(raw_responses, dict):
|
|
237
|
+
for key, val in raw_responses.items():
|
|
238
|
+
code_str = str(key)
|
|
239
|
+
if code_str.upper().startswith("HTTP_"):
|
|
240
|
+
code_str = code_str.split("HTTP_", 1)[1]
|
|
241
|
+
normalized[code_str] = val
|
|
242
|
+
return normalized
|
|
243
|
+
|
|
244
|
+
def _setup_security_schemes(self, inner: Any) -> None:
|
|
245
|
+
"""Configure security schemes from config."""
|
|
246
|
+
security_schemes = self.config.to_openapi_security_schemes()
|
|
247
|
+
|
|
248
|
+
if not security_schemes:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# Try flask-pydantic-spec's component interface first
|
|
253
|
+
comps: Any = getattr(inner, 'components', None)
|
|
254
|
+
if comps is not None and hasattr(comps, 'security_scheme'):
|
|
255
|
+
for scheme_name, scheme_config in security_schemes.items():
|
|
256
|
+
getattr(comps, 'security_scheme')(scheme_name, scheme_config)
|
|
257
|
+
# Fallback to direct dictionary manipulation
|
|
258
|
+
elif isinstance(inner, dict):
|
|
259
|
+
comp_dict = inner.setdefault('components', {})
|
|
260
|
+
schemes = comp_dict.setdefault('securitySchemes', {})
|
|
261
|
+
schemes.update(security_schemes)
|
|
262
|
+
except Exception:
|
|
263
|
+
# Silently fail to avoid breaking app initialization
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
def _setup_info_metadata(self, inner: Any) -> None:
|
|
267
|
+
"""Configure API metadata from config."""
|
|
268
|
+
info_data = self.config.to_openapi_info()
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
# Attempt to access info object directly
|
|
272
|
+
info_dict = getattr(inner, '_info', None)
|
|
273
|
+
target = (info_dict if isinstance(info_dict, dict)
|
|
274
|
+
else inner.setdefault('info', {}) if isinstance(inner, dict)
|
|
275
|
+
else None)
|
|
276
|
+
|
|
277
|
+
if target is not None:
|
|
278
|
+
target.update(info_data)
|
|
279
|
+
except Exception:
|
|
280
|
+
# Silently fail to avoid breaking app initialization
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
def _setup_servers(self, inner: Any) -> None:
|
|
284
|
+
"""Configure servers from config."""
|
|
285
|
+
if not self.config.servers:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
if isinstance(inner, dict):
|
|
290
|
+
inner['servers'] = self.config.servers
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def _setup_external_docs(self, inner: Any) -> None:
|
|
295
|
+
"""Configure external documentation from config."""
|
|
296
|
+
external_docs = self.config.to_openapi_external_docs()
|
|
297
|
+
if not external_docs:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
if isinstance(inner, dict):
|
|
302
|
+
inner['externalDocs'] = external_docs
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
def _apply_registered_endpoints(self, inner: Any) -> None:
|
|
307
|
+
"""Apply all endpoint metadata to OpenAPI spec."""
|
|
308
|
+
if not self._registered_endpoints:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Collect unique models for schema registration
|
|
312
|
+
models_to_register = set()
|
|
313
|
+
for _, _, metadata in self._registered_endpoints:
|
|
314
|
+
if metadata.request_body:
|
|
315
|
+
models_to_register.add(metadata.request_body)
|
|
316
|
+
for resp_model in metadata.responses.values():
|
|
317
|
+
if hasattr(resp_model, "model_json_schema"):
|
|
318
|
+
models_to_register.add(resp_model)
|
|
319
|
+
|
|
320
|
+
# Register Pydantic model schemas in components/schemas section
|
|
321
|
+
if models_to_register:
|
|
322
|
+
try:
|
|
323
|
+
comps: Any = getattr(inner, 'components', None)
|
|
324
|
+
if comps is not None and hasattr(comps, 'schema'):
|
|
325
|
+
# Use flask-pydantic-spec's schema registration method
|
|
326
|
+
for model in models_to_register:
|
|
327
|
+
comps.schema(model.__name__, model.model_json_schema())
|
|
328
|
+
elif isinstance(inner, dict):
|
|
329
|
+
# Direct dictionary manipulation fallback
|
|
330
|
+
schemas = inner.setdefault('components', {}).setdefault('schemas', {})
|
|
331
|
+
for model in models_to_register:
|
|
332
|
+
schemas[model.__name__] = model.model_json_schema()
|
|
333
|
+
except Exception:
|
|
334
|
+
# Continue even if schema registration fails
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
# Apply all metadata to each registered endpoint
|
|
338
|
+
for path, method, metadata in self._registered_endpoints:
|
|
339
|
+
operation_spec = {}
|
|
340
|
+
|
|
341
|
+
# Initialize parameters list
|
|
342
|
+
operation_spec['parameters'] = []
|
|
343
|
+
|
|
344
|
+
# Extract path parameters and add them to the spec
|
|
345
|
+
import re
|
|
346
|
+
path_params = re.findall(r'\{(\w+)\}', path)
|
|
347
|
+
for param_name in path_params:
|
|
348
|
+
operation_spec['parameters'].append({
|
|
349
|
+
'name': param_name,
|
|
350
|
+
'in': 'path',
|
|
351
|
+
'required': True,
|
|
352
|
+
'schema': {'type': 'string'},
|
|
353
|
+
'description': f'The {param_name} parameter'
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
# Add query parameters from metadata
|
|
357
|
+
for query_param in metadata.query_params:
|
|
358
|
+
operation_spec['parameters'].append({
|
|
359
|
+
'name': query_param.name,
|
|
360
|
+
'in': 'query',
|
|
361
|
+
'required': query_param.required,
|
|
362
|
+
'schema': {'type': query_param.type_},
|
|
363
|
+
'description': query_param.description,
|
|
364
|
+
**({'default': query_param.default} if query_param.default is not None else {})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
# Add request body if specified
|
|
368
|
+
if metadata.request_body:
|
|
369
|
+
operation_spec['requestBody'] = {
|
|
370
|
+
'required': True,
|
|
371
|
+
'content': {
|
|
372
|
+
'application/json': {
|
|
373
|
+
'schema': {'$ref': f"#/components/schemas/{metadata.request_body.__name__}"}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Add security requirements if specified
|
|
379
|
+
if metadata.security and metadata.security != SecurityScheme.NONE:
|
|
380
|
+
operation_spec['security'] = [{metadata.security.value: []}]
|
|
381
|
+
|
|
382
|
+
# Add tags, summary, description, etc.
|
|
383
|
+
if metadata.tags:
|
|
384
|
+
operation_spec['tags'] = metadata.tags
|
|
385
|
+
if metadata.summary:
|
|
386
|
+
operation_spec['summary'] = metadata.summary
|
|
387
|
+
if metadata.description:
|
|
388
|
+
operation_spec['description'] = metadata.description
|
|
389
|
+
if metadata.deprecated:
|
|
390
|
+
operation_spec['deprecated'] = True
|
|
391
|
+
|
|
392
|
+
# Add responses from decorator or normalized @spec.validate
|
|
393
|
+
if metadata.responses:
|
|
394
|
+
op_responses: Dict[str, Any] = {}
|
|
395
|
+
for code, model in metadata.responses.items():
|
|
396
|
+
code_str = str(code)
|
|
397
|
+
if code_str.upper().startswith("HTTP_"):
|
|
398
|
+
code_str = code_str.split("HTTP_", 1)[1]
|
|
399
|
+
if hasattr(model, "model_json_schema"):
|
|
400
|
+
op_responses[code_str] = {
|
|
401
|
+
"description": "Response",
|
|
402
|
+
"content": {
|
|
403
|
+
"application/json": {
|
|
404
|
+
"schema": {"$ref": f"#/components/schemas/{model.__name__}"}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
elif isinstance(model, dict):
|
|
409
|
+
op_responses[code_str] = {
|
|
410
|
+
"description": "Response",
|
|
411
|
+
"content": {
|
|
412
|
+
"application/json": {"schema": model}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else:
|
|
416
|
+
op_responses[code_str] = {"description": "Response"}
|
|
417
|
+
if op_responses:
|
|
418
|
+
operation_spec['responses'] = op_responses
|
|
419
|
+
|
|
420
|
+
# Add any extra metadata
|
|
421
|
+
operation_spec.update(metadata.extra_metadata)
|
|
422
|
+
|
|
423
|
+
# Apply the operation spec to the OpenAPI spec
|
|
424
|
+
if operation_spec:
|
|
425
|
+
# Direct dictionary manipulation for reliable metadata application
|
|
426
|
+
if isinstance(inner, dict):
|
|
427
|
+
# Get or create the path and method
|
|
428
|
+
if path not in inner.setdefault('paths', {}):
|
|
429
|
+
inner['paths'][path] = {}
|
|
430
|
+
if method not in inner['paths'][path]:
|
|
431
|
+
inner['paths'][path][method] = {}
|
|
432
|
+
|
|
433
|
+
ops = inner['paths'][path][method]
|
|
434
|
+
|
|
435
|
+
# Apply our custom metadata (this will override defaults)
|
|
436
|
+
for key, value in operation_spec.items():
|
|
437
|
+
if key == 'responses' and 'responses' in ops:
|
|
438
|
+
# Merge responses instead of overwriting
|
|
439
|
+
ops['responses'].update(value)
|
|
440
|
+
else:
|
|
441
|
+
ops[key] = value
|
|
442
|
+
|
|
443
|
+
# Add default response schemas if configured and not already present
|
|
444
|
+
if self.config.add_default_responses:
|
|
445
|
+
if 'responses' not in ops:
|
|
446
|
+
ops['responses'] = {}
|
|
447
|
+
|
|
448
|
+
if '200' not in ops['responses']:
|
|
449
|
+
ops['responses']['200'] = {
|
|
450
|
+
'description': 'Success',
|
|
451
|
+
'content': {
|
|
452
|
+
'application/json': {
|
|
453
|
+
'schema': {
|
|
454
|
+
'type': 'object',
|
|
455
|
+
'properties': {
|
|
456
|
+
'status': {'type': 'string', 'enum': ['success']},
|
|
457
|
+
'status_code': {'type': 'integer'},
|
|
458
|
+
'message': {'type': 'string'},
|
|
459
|
+
'data': {'type': 'object'}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if '400' not in ops['responses']:
|
|
466
|
+
ops['responses']['400'] = {
|
|
467
|
+
'description': 'Bad Request',
|
|
468
|
+
'content': {
|
|
469
|
+
'application/json': {
|
|
470
|
+
'schema': {
|
|
471
|
+
'type': 'object',
|
|
472
|
+
'properties': {
|
|
473
|
+
'status': {'type': 'string', 'enum': ['failed']},
|
|
474
|
+
'status_code': {'type': 'integer'},
|
|
475
|
+
'message': {'type': 'string'},
|
|
476
|
+
'data': {'type': 'object'}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quas-docs
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: A powerful, reusable package for generating comprehensive OpenAPI documentation with Flask-Pydantic-Spec
|
|
5
|
+
Home-page: https://github.com/zeddyemy/flask-openapi-docs
|
|
6
|
+
Author: Emmanuel Olowu
|
|
7
|
+
Author-email: Emmanuel Olowu <zeddyemy@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: flask,openapi,swagger,documentation,api,pydantic
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Framework :: Flask
|
|
17
|
+
Classifier: Topic :: Documentation
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: flask>=2.0.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Requires-Dist: flask-pydantic-spec>=0.8.6
|
|
25
|
+
Dynamic: author
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
|
|
29
|
+
# Flask OpenAPI Documentation Package
|
|
30
|
+
|
|
31
|
+
A powerful, reusable package for generating comprehensive OpenAPI documentation with Flask-Pydantic-Spec. Features custom metadata, security schemes, and flexible configuration options.
|
|
32
|
+
|
|
33
|
+
## ✨ Features
|
|
34
|
+
|
|
35
|
+
- **Flexible Configuration**: Easy-to-use configuration system for API metadata
|
|
36
|
+
- **Custom Security Schemes**: Support for multiple authentication types
|
|
37
|
+
- **Tag-based Organization**: Group endpoints by functionality
|
|
38
|
+
- **Route Format Control**: Choose between Flask (`<string:param>`) and OpenAPI (`{param}`) formats
|
|
39
|
+
- **Automatic Schema Registration**: Seamless Pydantic model integration
|
|
40
|
+
- **Backward Compatibility**: Drop-in replacement for existing setups
|
|
41
|
+
- **Zero Dependencies**: Only requires `flask-pydantic-spec` and `pydantic`
|
|
42
|
+
|
|
43
|
+
## 🚀 Quick Start
|
|
44
|
+
|
|
45
|
+
### Basic Usage
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from flask import Flask
|
|
49
|
+
from quas_docs import FlaskOpenAPISpec, DocsConfig, SecurityScheme, endpoint
|
|
50
|
+
|
|
51
|
+
# Create configuration
|
|
52
|
+
config = DocsConfig(
|
|
53
|
+
title="My API",
|
|
54
|
+
version="0.0.1",
|
|
55
|
+
description="A sample API with comprehensive documentation",
|
|
56
|
+
contact=ContactInfo(
|
|
57
|
+
email="api@example.com",
|
|
58
|
+
name="API Team"
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Initialize the spec
|
|
63
|
+
spec = FlaskOpenAPISpec(config)
|
|
64
|
+
|
|
65
|
+
# Create Flask app
|
|
66
|
+
app = Flask(__name__)
|
|
67
|
+
|
|
68
|
+
# Add endpoints with metadata
|
|
69
|
+
@app.post("/users")
|
|
70
|
+
@endpoint(
|
|
71
|
+
request_body=CreateUserRequest,
|
|
72
|
+
security=SecurityScheme.BEARER_AUTH,
|
|
73
|
+
tags=["Users"],
|
|
74
|
+
summary="Create New User",
|
|
75
|
+
description="Creates a new user account with validation"
|
|
76
|
+
)
|
|
77
|
+
def create_user():
|
|
78
|
+
return {"message": "User created"}
|
|
79
|
+
|
|
80
|
+
# Initialize documentation
|
|
81
|
+
spec.init_app(app)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Dynamic Configuration Methods
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from quas_docs import FlaskOpenAPISpec, DocsConfig
|
|
88
|
+
|
|
89
|
+
# Method 1: From dictionary (recommended)
|
|
90
|
+
config = DocsConfig.from_dict({
|
|
91
|
+
'title': 'My API',
|
|
92
|
+
'version': '2.0.0',
|
|
93
|
+
'description': 'API built with dynamic configuration',
|
|
94
|
+
'contact': {
|
|
95
|
+
'email': 'dev@mycompany.com',
|
|
96
|
+
'name': 'Development Team',
|
|
97
|
+
'url': 'https://mycompany.com'
|
|
98
|
+
},
|
|
99
|
+
'security_schemes': {
|
|
100
|
+
'BearerAuth': {'description': 'JWT authentication'},
|
|
101
|
+
'ApiKeyAuth': {
|
|
102
|
+
'scheme_type': 'apiKey',
|
|
103
|
+
'location': 'header',
|
|
104
|
+
'parameter_name': 'X-API-Key',
|
|
105
|
+
'description': 'API key authentication'
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
'preserve_flask_routes': True
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Method 2: From environment variables
|
|
112
|
+
config = DocsConfig.from_env(prefix="MY_API_")
|
|
113
|
+
|
|
114
|
+
# Method 3: Create default and customize
|
|
115
|
+
config = DocsConfig.create_default()
|
|
116
|
+
config.title = "Custom API"
|
|
117
|
+
config.version = "2.0.0"
|
|
118
|
+
|
|
119
|
+
# Method 4: Manual construction
|
|
120
|
+
config = DocsConfig(
|
|
121
|
+
title="Custom API",
|
|
122
|
+
version="2.0.0",
|
|
123
|
+
description="Custom API with specific requirements"
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 📋 Configuration Options
|
|
128
|
+
|
|
129
|
+
### DocsConfig Class
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
@dataclass
|
|
133
|
+
class DocsConfig:
|
|
134
|
+
# Basic API Information
|
|
135
|
+
title: str = "Flask API"
|
|
136
|
+
version: str = "0.0.1"
|
|
137
|
+
description: Optional[str] = None
|
|
138
|
+
terms_of_service: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
# Contact Information
|
|
141
|
+
contact: Optional[ContactInfo] = None
|
|
142
|
+
|
|
143
|
+
# License Information
|
|
144
|
+
license_name: Optional[str] = None
|
|
145
|
+
license_url: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
# Server Information
|
|
148
|
+
servers: List[Dict[str, str]] = field(default_factory=list)
|
|
149
|
+
|
|
150
|
+
# Security Schemes
|
|
151
|
+
security_schemes: Dict[str, SecuritySchemeConfig] = field(default_factory=dict)
|
|
152
|
+
|
|
153
|
+
# Customization Options
|
|
154
|
+
preserve_flask_routes: bool = True # Keep <string:param> format
|
|
155
|
+
clear_auto_discovered: bool = True # Remove auto-discovered duplicates
|
|
156
|
+
add_default_responses: bool = True # Add default response schemas
|
|
157
|
+
|
|
158
|
+
# External Documentation
|
|
159
|
+
external_docs_url: Optional[str] = None
|
|
160
|
+
external_docs_description: Optional[str] = None
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Security Schemes
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from quas_docs import SecuritySchemeConfig
|
|
167
|
+
|
|
168
|
+
# API Key Authentication
|
|
169
|
+
api_key_config = SecuritySchemeConfig(
|
|
170
|
+
name="ApiKeyAuth",
|
|
171
|
+
scheme_type="apiKey",
|
|
172
|
+
location="header",
|
|
173
|
+
parameter_name="X-API-Key",
|
|
174
|
+
description="API key authentication"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Bearer Token Authentication
|
|
178
|
+
bearer_config = SecuritySchemeConfig(
|
|
179
|
+
name="BearerAuth",
|
|
180
|
+
scheme_type="apiKey",
|
|
181
|
+
location="header",
|
|
182
|
+
parameter_name="Authorization",
|
|
183
|
+
description="JWT Bearer token authentication"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Add to configuration
|
|
187
|
+
config.add_security_scheme("ApiKeyAuth", api_key_config)
|
|
188
|
+
config.add_security_scheme("BearerAuth", bearer_config)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Contact Information
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from quas_docs import ContactInfo
|
|
195
|
+
|
|
196
|
+
contact = ContactInfo(
|
|
197
|
+
email="api@example.com",
|
|
198
|
+
name="API Development Team",
|
|
199
|
+
url="https://example.com/contact"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
config.contact = contact
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 🎯 Endpoint Decoration
|
|
206
|
+
|
|
207
|
+
### The @endpoint Decorator
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
@endpoint(
|
|
211
|
+
request_body=Optional[Type[BaseModel]], # Pydantic model for request body
|
|
212
|
+
security=Optional[SecurityScheme], # Security requirement
|
|
213
|
+
tags=Optional[List[str]], # Organization tags
|
|
214
|
+
summary=Optional[str], # Brief description
|
|
215
|
+
description=Optional[str], # Detailed description
|
|
216
|
+
query_params=Optional[List[QueryParameter]], # Query parameters for GET endpoints
|
|
217
|
+
deprecated=bool, # Mark as deprecated
|
|
218
|
+
**extra_metadata # Custom metadata
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Examples
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# Basic endpoint with request body
|
|
226
|
+
@app.post("/auth/login")
|
|
227
|
+
@endpoint(
|
|
228
|
+
request_body=LoginRequest,
|
|
229
|
+
tags=["Authentication"],
|
|
230
|
+
summary="User Login",
|
|
231
|
+
description="Authenticate user with email and password"
|
|
232
|
+
)
|
|
233
|
+
@spec.validate(resp=Response(HTTP_200=ApiResponse))
|
|
234
|
+
def login():
|
|
235
|
+
return AuthController.login()
|
|
236
|
+
|
|
237
|
+
# Secured endpoint with query parameters
|
|
238
|
+
@app.get("/users")
|
|
239
|
+
@endpoint(
|
|
240
|
+
security=SecurityScheme.BEARER_AUTH,
|
|
241
|
+
tags=["Users"],
|
|
242
|
+
summary="List Users",
|
|
243
|
+
description="Get paginated list of users with filtering options",
|
|
244
|
+
query_params=[
|
|
245
|
+
QueryParameter("page", "integer", required=False, description="Page number", default=1),
|
|
246
|
+
QueryParameter("per_page", "integer", required=False, description="Items per page", default=10),
|
|
247
|
+
QueryParameter("search", "string", required=False, description="Search by name or email"),
|
|
248
|
+
QueryParameter("active", "boolean", required=False, description="Filter by active status", default=True),
|
|
249
|
+
]
|
|
250
|
+
)
|
|
251
|
+
@spec.validate(resp=Response(HTTP_200=UsersListResponse))
|
|
252
|
+
def list_users():
|
|
253
|
+
return UserController.list()
|
|
254
|
+
|
|
255
|
+
# Public endpoint with custom metadata
|
|
256
|
+
@app.get("/health")
|
|
257
|
+
@endpoint(
|
|
258
|
+
tags=["System"],
|
|
259
|
+
summary="Health Check",
|
|
260
|
+
description="Check API health status",
|
|
261
|
+
custom_field="health-check" # Custom metadata
|
|
262
|
+
)
|
|
263
|
+
def health_check():
|
|
264
|
+
return {"status": "healthy"}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## 🔧 Advanced Configuration
|
|
268
|
+
|
|
269
|
+
### Custom Response Schemas
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# The package automatically adds default response schemas
|
|
273
|
+
# You can disable this behavior:
|
|
274
|
+
config.add_default_responses = False
|
|
275
|
+
|
|
276
|
+
# Custom response schemas are preserved from @spec.validate decorators
|
|
277
|
+
@app.post("/users")
|
|
278
|
+
@endpoint(tags=["Users"])
|
|
279
|
+
@spec.validate(resp=Response(
|
|
280
|
+
HTTP_201=CreateUserResponse,
|
|
281
|
+
HTTP_400=ErrorResponse,
|
|
282
|
+
HTTP_409=ConflictResponse
|
|
283
|
+
))
|
|
284
|
+
def create_user():
|
|
285
|
+
return UserController.create()
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Route Format Control
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
# Keep Flask format: /users/<string:user_id>
|
|
292
|
+
config.preserve_flask_routes = True
|
|
293
|
+
|
|
294
|
+
# Use OpenAPI format: /users/{user_id}
|
|
295
|
+
config.preserve_flask_routes = False
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Servers Configuration
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
# Add multiple servers
|
|
302
|
+
config.add_server("https://api.example.com", "Production")
|
|
303
|
+
config.add_server("https://staging-api.example.com", "Staging")
|
|
304
|
+
config.add_server("http://localhost:5000", "Development")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### External Documentation
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
config.external_docs_url = "https://docs.example.com"
|
|
311
|
+
config.external_docs_description = "Complete API Documentation"
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Environment Variable Configuration
|
|
315
|
+
|
|
316
|
+
Set environment variables and load them automatically:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# .env file or environment
|
|
320
|
+
export API_TITLE="My Project API"
|
|
321
|
+
export API_VERSION="1.2.0"
|
|
322
|
+
export API_DESCRIPTION="API for my awesome project"
|
|
323
|
+
export API_CONTACT_EMAIL="api@myproject.com"
|
|
324
|
+
export API_CONTACT_NAME="API Team"
|
|
325
|
+
export API_CONTACT_URL="https://myproject.com/contact"
|
|
326
|
+
export API_LICENSE_NAME="MIT"
|
|
327
|
+
export API_PRESERVE_FLASK_ROUTES="true"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
# Load configuration from environment
|
|
332
|
+
config = DocsConfig.from_env() # Uses API_ prefix by default
|
|
333
|
+
|
|
334
|
+
# Or use custom prefix
|
|
335
|
+
config = DocsConfig.from_env(prefix="MYAPI_")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## 📦 Integration Examples
|
|
339
|
+
|
|
340
|
+
### Replace Existing Setup
|
|
341
|
+
|
|
342
|
+
If you have an existing Flask app with manual OpenAPI setup:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
# OLD WAY
|
|
346
|
+
from flask_pydantic_spec import FlaskPydanticSpec
|
|
347
|
+
spec = FlaskPydanticSpec('flask', title='My API', version='0.0.1')
|
|
348
|
+
|
|
349
|
+
# NEW WAY
|
|
350
|
+
from quas_docs import FlaskOpenAPISpec, DocsConfig
|
|
351
|
+
config = DocsConfig(title='My API', version='0.0.1')
|
|
352
|
+
spec_instance = FlaskOpenAPISpec(config)
|
|
353
|
+
spec = spec_instance.spec # For backward compatibility
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Multiple APIs
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
# API 1: Public API
|
|
360
|
+
public_config = DocsConfig(
|
|
361
|
+
title="Public API",
|
|
362
|
+
version="0.0.1",
|
|
363
|
+
preserve_flask_routes=True
|
|
364
|
+
)
|
|
365
|
+
public_spec = FlaskOpenAPISpec(public_config)
|
|
366
|
+
|
|
367
|
+
# API 2: Admin API
|
|
368
|
+
admin_config = DocsConfig(
|
|
369
|
+
title="Admin API",
|
|
370
|
+
version="0.0.1",
|
|
371
|
+
preserve_flask_routes=False
|
|
372
|
+
)
|
|
373
|
+
admin_spec = FlaskOpenAPISpec(admin_config)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Custom Project Setup
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
# projects/my_project/docs_config.py
|
|
380
|
+
from quas_docs import DocsConfig, ContactInfo, SecuritySchemeConfig
|
|
381
|
+
|
|
382
|
+
def create_my_project_config():
|
|
383
|
+
config = DocsConfig(
|
|
384
|
+
title="My Project API",
|
|
385
|
+
version="2.1.0",
|
|
386
|
+
description="Custom project with specific requirements",
|
|
387
|
+
contact=ContactInfo(
|
|
388
|
+
email="dev@myproject.com",
|
|
389
|
+
name="Development Team",
|
|
390
|
+
url="https://myproject.com"
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Add custom security schemes
|
|
395
|
+
config.add_security_scheme("ApiKey", SecuritySchemeConfig(
|
|
396
|
+
name="ApiKey",
|
|
397
|
+
parameter_name="X-API-Key",
|
|
398
|
+
description="Project-specific API key"
|
|
399
|
+
))
|
|
400
|
+
|
|
401
|
+
config.add_server("https://api.myproject.com", "Production")
|
|
402
|
+
config.add_server("http://localhost:8000", "Development")
|
|
403
|
+
|
|
404
|
+
return config
|
|
405
|
+
|
|
406
|
+
# projects/my_project/app.py
|
|
407
|
+
from quas_docs import FlaskOpenAPISpec
|
|
408
|
+
from .docs_config import create_my_project_config
|
|
409
|
+
|
|
410
|
+
config = create_my_project_config()
|
|
411
|
+
spec = FlaskOpenAPISpec(config)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## 🔄 Migration Guide
|
|
415
|
+
|
|
416
|
+
### From Manual Setup
|
|
417
|
+
|
|
418
|
+
1. **Copy the docs/ folder** to your project
|
|
419
|
+
2. **Replace your existing docs setup**:
|
|
420
|
+
```python
|
|
421
|
+
# Replace your old setup with:
|
|
422
|
+
from quas_docs import FlaskOpenAPISpec, DocsConfig, endpoint, SecurityScheme
|
|
423
|
+
|
|
424
|
+
config = DocsConfig.from_dict({
|
|
425
|
+
'title': 'Your API Name',
|
|
426
|
+
'version': '0.0.1',
|
|
427
|
+
# ... your settings
|
|
428
|
+
})
|
|
429
|
+
spec_instance = FlaskOpenAPISpec(config)
|
|
430
|
+
spec = spec_instance.spec # For @spec.validate decorators
|
|
431
|
+
```
|
|
432
|
+
3. **Use the @endpoint decorator**:
|
|
433
|
+
```python
|
|
434
|
+
@endpoint(
|
|
435
|
+
request_body=YourModel,
|
|
436
|
+
security=SecurityScheme.BEARER_AUTH,
|
|
437
|
+
tags=["Your Tag"],
|
|
438
|
+
summary="Your Summary"
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
4. **Initialize documentation**:
|
|
442
|
+
```python
|
|
443
|
+
spec_instance.init_app(app)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Clean v1.0 Design
|
|
447
|
+
|
|
448
|
+
The package follows a clean, modern approach:
|
|
449
|
+
|
|
450
|
+
- Single `@endpoint` decorator for all metadata
|
|
451
|
+
- Configuration-driven setup
|
|
452
|
+
- No legacy compatibility code
|
|
453
|
+
- Streamlined API surface
|
|
454
|
+
|
|
455
|
+
## 📝 Best Practices
|
|
456
|
+
|
|
457
|
+
1. **Use meaningful tags** to organize endpoints logically
|
|
458
|
+
2. **Provide clear summaries and descriptions** for better developer experience
|
|
459
|
+
3. **Configure contact information** for API support
|
|
460
|
+
4. **Use appropriate security schemes** for different endpoint types
|
|
461
|
+
5. **Test documentation** in both Swagger UI and Redoc
|
|
462
|
+
6. **Version your APIs** properly using semantic versioning
|
|
463
|
+
|
|
464
|
+
## 🐛 Troubleshooting
|
|
465
|
+
|
|
466
|
+
### Common Issues
|
|
467
|
+
|
|
468
|
+
**Issue**: Endpoints appear in "default" category
|
|
469
|
+
**Solution**: Ensure `clear_auto_discovered = True` in config
|
|
470
|
+
|
|
471
|
+
**Issue**: Route parameters show wrong format
|
|
472
|
+
**Solution**: Set `preserve_flask_routes` in config
|
|
473
|
+
|
|
474
|
+
**Issue**: Security schemes not working
|
|
475
|
+
**Solution**: Verify security scheme names match those in config
|
|
476
|
+
|
|
477
|
+
**Issue**: Missing response schemas
|
|
478
|
+
**Solution**: Check `add_default_responses` setting and `@spec.validate` decorators
|
|
479
|
+
|
|
480
|
+
## 📄 License
|
|
481
|
+
|
|
482
|
+
MIT License - feel free to use in your projects!
|
|
483
|
+
|
|
484
|
+
## 🤝 Contributing
|
|
485
|
+
|
|
486
|
+
1. Fork the repository
|
|
487
|
+
2. Create a feature branch
|
|
488
|
+
3. Add tests for new functionality
|
|
489
|
+
4. Submit a pull request
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
**Created by Emmanuel Olowu** | [GitHub](https://github.com/zeddyemy) | [Website](https://eshomonu.com/)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
quas_docs/__init__.py,sha256=x_JjDsweqFwPVfOfcB5RC4TUl_hChJi9Q4UK03EbUwo,804
|
|
2
|
+
quas_docs/config.py,sha256=Kba_V7ClD0eT8qaCuG6RYJRpde_AMK-gPjEWmO8Y6zI,9198
|
|
3
|
+
quas_docs/core.py,sha256=zOSGhhZ9b1REiTLHKCw_glBKNXPcgJbdzeUOAtrZvR8,20093
|
|
4
|
+
quas_docs-0.0.3.dist-info/METADATA,sha256=f4Zs4tlqBKIk3WdQjDlhJ9UxeqQuS0c5d4DlkOFplAQ,14152
|
|
5
|
+
quas_docs-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
quas_docs-0.0.3.dist-info/top_level.txt,sha256=Yr3-tV9MrBuJ2wZ-mKqpg_rHtUsFrXzKWhApgY8V_EE,10
|
|
7
|
+
quas_docs-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
quas_docs
|