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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ quas_docs