flask-ast-openapi 0.2.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (19) hide show
  1. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/PKG-INFO +24 -5
  2. flask_ast_openapi-0.2.0/src/flask_ast_openapi.egg-info/PKG-INFO → flask_ast_openapi-0.2.1/README.md +37 -30
  3. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/pyproject.toml +1 -1
  4. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi/generator.py +112 -27
  5. flask_ast_openapi-0.2.0/README.md → flask_ast_openapi-0.2.1/src/flask_ast_openapi.egg-info/PKG-INFO +46 -15
  6. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/tests/test_generator.py +86 -0
  7. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/LICENSE +0 -0
  8. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/setup.cfg +0 -0
  9. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi/__init__.py +0 -0
  10. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi/cli.py +0 -0
  11. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi/swagger.py +0 -0
  12. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi.egg-info/SOURCES.txt +0 -0
  13. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi.egg-info/dependency_links.txt +0 -0
  14. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi.egg-info/entry_points.txt +0 -0
  15. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi.egg-info/requires.txt +0 -0
  16. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/src/flask_ast_openapi.egg-info/top_level.txt +0 -0
  17. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/tests/test_cli.py +0 -0
  18. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/tests/test_package.py +0 -0
  19. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.1}/tests/test_swagger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-ast-openapi
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Generate OpenAPI specifications from Flask source code using AST
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -55,6 +55,8 @@ It also provides optional Swagger UI integration for Flask applications.
55
55
  * Marshmallow request schema support
56
56
  * Marshmallow response schema support
57
57
  * Cross-file Marshmallow schema resolution
58
+ * Automatic request content-type detection for JSON and multipart forms
59
+ * Request and response content-type docstring overrides
58
60
  * Nested Marshmallow schema support
59
61
  * OpenAPI component schema generation
60
62
  * Configurable authentication decorators
@@ -431,6 +433,23 @@ def create_user():
431
433
  ...
432
434
  ```
433
435
 
436
+ JSON bodies are detected from `request.get_json()` and `request.json`.
437
+ Multipart bodies are detected from `request.form` and `request.files`.
438
+ For custom media types, use docstring directives:
439
+
440
+ ```python
441
+ @app.post("/documents")
442
+ def upload_document():
443
+ """
444
+ :request: DocumentUploadSchema
445
+ :response: DocumentResponseSchema
446
+ :request-content-type: multipart/form-data
447
+ :response-content-type: application/json
448
+ """
449
+
450
+ ...
451
+ ```
452
+
434
453
  The generator detects `UserRequestSchema` and creates an OpenAPI request-body schema.
435
454
 
436
455
  ## Response Schemas
@@ -785,12 +804,12 @@ Future development may include:
785
804
  Current version:
786
805
 
787
806
  ```text
788
- 0.2.0
807
+ 0.2.1
789
808
  ```
790
809
 
791
- Version `0.2.0` adds nested cross-file Blueprint prefix resolution,
792
- automatic Blueprint tags, cross-file Marshmallow schema resolution, and
793
- default exclusion of test and virtual-environment directories.
810
+ Version `0.2.1` adds automatic JSON and multipart request content-type
811
+ detection, explicit request/response media-type overrides, and avoids creating
812
+ request bodies for header-only and path-only inputs.
794
813
 
795
814
  ## License
796
815
 
@@ -1,15 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: flask-ast-openapi
3
- Version: 0.2.0
4
- Summary: Generate OpenAPI specifications from Flask source code using AST
5
- Requires-Python: >=3.10
6
- Description-Content-Type: text/markdown
7
- License-File: LICENSE
8
- Requires-Dist: Flask>=3.0
9
- Provides-Extra: dev
10
- Requires-Dist: pytest>=8; extra == "dev"
11
- Dynamic: license-file
12
-
13
1
  # flask-ast-openapi
14
2
 
15
3
  Generate OpenAPI 3 specifications from Flask source code using Python's Abstract Syntax Tree (AST).
@@ -31,10 +19,10 @@ It also provides optional Swagger UI integration for Flask applications.
31
19
  * `@app.put(...)`
32
20
  * `@app.patch(...)`
33
21
  * `@app.delete(...)`
34
- * Blueprint route support
35
- * Blueprint `url_prefix` support
36
- * Nested and cross-file Blueprint registration support
37
- * Automatic Blueprint tags for Swagger UI grouping
22
+ * Blueprint route support
23
+ * Blueprint `url_prefix` support
24
+ * Nested and cross-file Blueprint registration support
25
+ * Automatic Blueprint tags for Swagger UI grouping
38
26
  * Flask path parameter conversion to OpenAPI format
39
27
  * Flask converter support:
40
28
 
@@ -53,8 +41,10 @@ It also provides optional Swagger UI integration for Flask applications.
53
41
  * Function docstrings as OpenAPI descriptions
54
42
  * Synchronous and asynchronous Flask route support
55
43
  * Marshmallow request schema support
56
- * Marshmallow response schema support
57
- * Cross-file Marshmallow schema resolution
44
+ * Marshmallow response schema support
45
+ * Cross-file Marshmallow schema resolution
46
+ * Automatic request content-type detection for JSON and multipart forms
47
+ * Request and response content-type docstring overrides
58
48
  * Nested Marshmallow schema support
59
49
  * OpenAPI component schema generation
60
50
  * Configurable authentication decorators
@@ -417,7 +407,7 @@ class UserRequestSchema(Schema):
417
407
  active = fields.Boolean()
418
408
  ```
419
409
 
420
- A route can reference the schema from its docstring:
410
+ A route can reference the schema from its docstring:
421
411
 
422
412
  ```python
423
413
  @app.post("/users")
@@ -428,8 +418,25 @@ def create_user():
428
418
  :request: UserRequestSchema
429
419
  """
430
420
 
431
- ...
432
- ```
421
+ ...
422
+ ```
423
+
424
+ JSON bodies are detected from `request.get_json()` and `request.json`.
425
+ Multipart bodies are detected from `request.form` and `request.files`.
426
+ For custom media types, use docstring directives:
427
+
428
+ ```python
429
+ @app.post("/documents")
430
+ def upload_document():
431
+ """
432
+ :request: DocumentUploadSchema
433
+ :response: DocumentResponseSchema
434
+ :request-content-type: multipart/form-data
435
+ :response-content-type: application/json
436
+ """
437
+
438
+ ...
439
+ ```
433
440
 
434
441
  The generator detects `UserRequestSchema` and creates an OpenAPI request-body schema.
435
442
 
@@ -782,15 +789,15 @@ Future development may include:
782
789
 
783
790
  ## Version
784
791
 
785
- Current version:
786
-
787
- ```text
788
- 0.2.0
789
- ```
790
-
791
- Version `0.2.0` adds nested cross-file Blueprint prefix resolution,
792
- automatic Blueprint tags, cross-file Marshmallow schema resolution, and
793
- default exclusion of test and virtual-environment directories.
792
+ Current version:
793
+
794
+ ```text
795
+ 0.2.1
796
+ ```
797
+
798
+ Version `0.2.1` adds automatic JSON and multipart request content-type
799
+ detection, explicit request/response media-type overrides, and avoids creating
800
+ request bodies for header-only and path-only inputs.
794
801
 
795
802
  ## License
796
803
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "flask-ast-openapi"
8
- version = "0.2.0"
8
+ version = "0.2.1"
9
9
  description = "Generate OpenAPI specifications from Flask source code using AST"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -32,8 +32,10 @@ class RouteDefinition:
32
32
  auth_schemes: list[str] = field(default_factory=list)
33
33
  auth_scheme_mode: str = "and"
34
34
  description: str | None = None
35
- request_schema: dict[str, Any] | None = None
35
+ request_schema: dict[str, Any] | None = None
36
36
  response_schema: dict[str, Any] | None = None
37
+ request_content_type: str | None = "application/json"
38
+ response_content_type: str = "application/json"
37
39
  tag: str | None = None
38
40
  @dataclass
39
41
  class PathParameter:
@@ -283,10 +285,16 @@ class FlaskASTOpenAPI:
283
285
  function
284
286
  )
285
287
  ),
286
- request_schema=self.build_request_schema_from_docstring(
287
- tree,
288
- function,
289
- ),
288
+ request_schema=self.build_request_schema_from_docstring(
289
+ tree,
290
+ function,
291
+ ),
292
+ request_content_type=(
293
+ self.extract_request_content_type(function)
294
+ ),
295
+ response_content_type=(
296
+ self.extract_response_content_type(function)
297
+ ),
290
298
  uses_json_body=(
291
299
  self.function_uses_json_body(
292
300
  function
@@ -451,16 +459,20 @@ class FlaskASTOpenAPI:
451
459
  operation: dict[str, Any] = {
452
460
  "operationId": route.function_name,
453
461
  "parameters": parameters,
454
- "responses": self.build_openapi_responses(
455
- response_status_codes,
456
- response_schemas,
457
- ),
462
+ "responses": self.build_openapi_responses(
463
+ response_status_codes,
464
+ response_schemas,
465
+ route.response_content_type,
466
+ ),
458
467
  }
459
468
 
460
469
  if route.tag:
461
470
  operation["tags"] = [route.tag]
462
471
 
463
- if route.uses_json_body or route.request_schema is not None:
472
+ if (
473
+ route.request_content_type is not None
474
+ and (route.uses_json_body or route.request_schema is not None)
475
+ ):
464
476
  request_schema = route.request_schema
465
477
 
466
478
  if request_schema is None:
@@ -471,12 +483,12 @@ class FlaskASTOpenAPI:
471
483
  )
472
484
 
473
485
  operation["requestBody"] = {
474
- "required": True,
475
- "content": {
476
- "application/json": {
477
- "schema": request_schema,
478
- }
479
- },
486
+ "required": True,
487
+ "content": {
488
+ route.request_content_type: {
489
+ "schema": request_schema,
490
+ }
491
+ },
480
492
  }
481
493
 
482
494
  if route.description:
@@ -795,7 +807,7 @@ class FlaskASTOpenAPI:
795
807
  }
796
808
  for name in names
797
809
  ]
798
- def function_uses_json_body(self,function: ast.FunctionDef | ast.AsyncFunctionDef,) -> bool:
810
+ def function_uses_json_body(self,function: ast.FunctionDef | ast.AsyncFunctionDef,) -> bool:
799
811
  """Check whether a route function reads a JSON request body."""
800
812
 
801
813
  for node in ast.walk(function):
@@ -816,7 +828,79 @@ class FlaskASTOpenAPI:
816
828
  ):
817
829
  return True
818
830
 
819
- return False
831
+ return False
832
+
833
+ def function_uses_multipart_body(
834
+ self,
835
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
836
+ ) -> bool:
837
+ """Check whether a route reads form fields or uploaded files."""
838
+
839
+ for node in ast.walk(function):
840
+ if not isinstance(node, ast.Attribute):
841
+ continue
842
+
843
+ if (
844
+ isinstance(node.value, ast.Name)
845
+ and node.value.id == "request"
846
+ and node.attr in {"form", "files"}
847
+ ):
848
+ return True
849
+
850
+ return False
851
+
852
+ def extract_docstring_directive(
853
+ self,
854
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
855
+ directive: str,
856
+ ) -> str | None:
857
+ """Extract a named ``:directive: value`` from a docstring."""
858
+
859
+ docstring = ast.get_docstring(function)
860
+ if not docstring:
861
+ return None
862
+
863
+ match = re.search(
864
+ rf"^\s*:{re.escape(directive)}:\s*([^\s]+)",
865
+ docstring,
866
+ re.MULTILINE,
867
+ )
868
+ return match.group(1) if match else None
869
+
870
+ def extract_request_content_type(
871
+ self,
872
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
873
+ ) -> str | None:
874
+ """Infer request body media type, allowing a docstring override."""
875
+
876
+ explicit = self.extract_docstring_directive(
877
+ function,
878
+ "request-content-type",
879
+ )
880
+ if explicit:
881
+ return explicit
882
+
883
+ if self.function_uses_multipart_body(function):
884
+ return "multipart/form-data"
885
+
886
+ if self.function_uses_json_body(function):
887
+ return "application/json"
888
+
889
+ return None
890
+
891
+ def extract_response_content_type(
892
+ self,
893
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
894
+ ) -> str:
895
+ """Return the declared response media type or JSON by default."""
896
+
897
+ return (
898
+ self.extract_docstring_directive(
899
+ function,
900
+ "response-content-type",
901
+ )
902
+ or "application/json"
903
+ )
820
904
  def extract_json_body_field_names(self,function: ast.FunctionDef | ast.AsyncFunctionDef,) -> list[str]:
821
905
  """Extract JSON body field names used inside a route function."""
822
906
 
@@ -1151,10 +1235,11 @@ class FlaskASTOpenAPI:
1151
1235
  status_codes.append(status_code)
1152
1236
 
1153
1237
  return status_codes
1154
- def build_openapi_responses(
1155
- self,
1156
- status_codes: list[int],
1157
- response_schemas: dict[int, dict[str, Any]] | None = None,
1238
+ def build_openapi_responses(
1239
+ self,
1240
+ status_codes: list[int],
1241
+ response_schemas: dict[int, dict[str, Any]] | None = None,
1242
+ content_type: str = "application/json",
1158
1243
  ) -> dict[str, Any]:
1159
1244
  """Build OpenAPI responses from status codes and schemas."""
1160
1245
 
@@ -1176,11 +1261,11 @@ class FlaskASTOpenAPI:
1176
1261
  schema = response_schemas.get(status_code)
1177
1262
 
1178
1263
  if schema:
1179
- response["content"] = {
1180
- "application/json": {
1181
- "schema": schema,
1182
- }
1183
- }
1264
+ response["content"] = {
1265
+ content_type: {
1266
+ "schema": schema,
1267
+ }
1268
+ }
1184
1269
 
1185
1270
  responses[str(status_code)] = response
1186
1271
 
@@ -1,3 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask-ast-openapi
3
+ Version: 0.2.1
4
+ Summary: Generate OpenAPI specifications from Flask source code using AST
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: Flask>=3.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8; extra == "dev"
11
+ Dynamic: license-file
12
+
1
13
  # flask-ast-openapi
2
14
 
3
15
  Generate OpenAPI 3 specifications from Flask source code using Python's Abstract Syntax Tree (AST).
@@ -19,10 +31,10 @@ It also provides optional Swagger UI integration for Flask applications.
19
31
  * `@app.put(...)`
20
32
  * `@app.patch(...)`
21
33
  * `@app.delete(...)`
22
- * Blueprint route support
23
- * Blueprint `url_prefix` support
24
- * Nested and cross-file Blueprint registration support
25
- * Automatic Blueprint tags for Swagger UI grouping
34
+ * Blueprint route support
35
+ * Blueprint `url_prefix` support
36
+ * Nested and cross-file Blueprint registration support
37
+ * Automatic Blueprint tags for Swagger UI grouping
26
38
  * Flask path parameter conversion to OpenAPI format
27
39
  * Flask converter support:
28
40
 
@@ -41,8 +53,10 @@ It also provides optional Swagger UI integration for Flask applications.
41
53
  * Function docstrings as OpenAPI descriptions
42
54
  * Synchronous and asynchronous Flask route support
43
55
  * Marshmallow request schema support
44
- * Marshmallow response schema support
45
- * Cross-file Marshmallow schema resolution
56
+ * Marshmallow response schema support
57
+ * Cross-file Marshmallow schema resolution
58
+ * Automatic request content-type detection for JSON and multipart forms
59
+ * Request and response content-type docstring overrides
46
60
  * Nested Marshmallow schema support
47
61
  * OpenAPI component schema generation
48
62
  * Configurable authentication decorators
@@ -419,6 +433,23 @@ def create_user():
419
433
  ...
420
434
  ```
421
435
 
436
+ JSON bodies are detected from `request.get_json()` and `request.json`.
437
+ Multipart bodies are detected from `request.form` and `request.files`.
438
+ For custom media types, use docstring directives:
439
+
440
+ ```python
441
+ @app.post("/documents")
442
+ def upload_document():
443
+ """
444
+ :request: DocumentUploadSchema
445
+ :response: DocumentResponseSchema
446
+ :request-content-type: multipart/form-data
447
+ :response-content-type: application/json
448
+ """
449
+
450
+ ...
451
+ ```
452
+
422
453
  The generator detects `UserRequestSchema` and creates an OpenAPI request-body schema.
423
454
 
424
455
  ## Response Schemas
@@ -770,15 +801,15 @@ Future development may include:
770
801
 
771
802
  ## Version
772
803
 
773
- Current version:
774
-
775
- ```text
776
- 0.2.0
777
- ```
778
-
779
- Version `0.2.0` adds nested cross-file Blueprint prefix resolution,
780
- automatic Blueprint tags, cross-file Marshmallow schema resolution, and
781
- default exclusion of test and virtual-environment directories.
804
+ Current version:
805
+
806
+ ```text
807
+ 0.2.1
808
+ ```
809
+
810
+ Version `0.2.1` adds automatic JSON and multipart request content-type
811
+ detection, explicit request/response media-type overrides, and avoids creating
812
+ request bodies for header-only and path-only inputs.
782
813
 
783
814
  ## License
784
815
 
@@ -6405,3 +6405,89 @@ def create_user():
6405
6405
  response_schema = operation["responses"]["200"]["content"]["application/json"]["schema"]
6406
6406
  assert request_schema["properties"]["name"] == {"type": "string"}
6407
6407
  assert response_schema["properties"]["id"] == {"type": "integer"}
6408
+
6409
+
6410
+ def test_generate_infers_multipart_form_data(tmp_path):
6411
+ (tmp_path / "routes.py").write_text(
6412
+ """
6413
+ from flask import Blueprint, request
6414
+ from marshmallow import Schema, fields
6415
+
6416
+ class UploadSchema(Schema):
6417
+ title = fields.String(required=True)
6418
+ file = fields.Raw(required=True)
6419
+
6420
+ upload_bp = Blueprint("upload", __name__)
6421
+
6422
+ @upload_bp.route("/upload", methods=["POST"])
6423
+ def upload():
6424
+ \"\"\":request: UploadSchema\"\"\"
6425
+ title = request.form.get("title")
6426
+ file = request.files.get("file")
6427
+ return {}, 200
6428
+ """,
6429
+ encoding="utf-8",
6430
+ )
6431
+
6432
+ spec = FlaskASTOpenAPI(tmp_path).generate()
6433
+ content = spec["paths"]["/upload"]["post"]["requestBody"]["content"]
6434
+
6435
+ assert "multipart/form-data" in content
6436
+ assert "application/json" not in content
6437
+
6438
+
6439
+ def test_generate_does_not_create_body_for_header_only_schema(tmp_path):
6440
+ (tmp_path / "routes.py").write_text(
6441
+ """
6442
+ from flask import Blueprint, request
6443
+ from marshmallow import Schema, fields
6444
+
6445
+ class TokenSchema(Schema):
6446
+ token = fields.String(required=True)
6447
+
6448
+ auth_bp = Blueprint("auth", __name__)
6449
+
6450
+ @auth_bp.route("/validate", methods=["POST"])
6451
+ def validate():
6452
+ \"\"\":request: TokenSchema\"\"\"
6453
+ token = request.headers.get("Authorization")
6454
+ return {}, 200
6455
+ """,
6456
+ encoding="utf-8",
6457
+ )
6458
+
6459
+ spec = FlaskASTOpenAPI(tmp_path).generate()
6460
+
6461
+ assert "requestBody" not in spec["paths"]["/validate"]["post"]
6462
+
6463
+
6464
+ def test_docstring_can_override_request_and_response_content_types(tmp_path):
6465
+ (tmp_path / "routes.py").write_text(
6466
+ """
6467
+ from flask import Blueprint, request
6468
+ from marshmallow import Schema, fields
6469
+
6470
+ class PayloadSchema(Schema):
6471
+ value = fields.String(required=True)
6472
+
6473
+ bp = Blueprint("custom", __name__)
6474
+
6475
+ @bp.route("/custom", methods=["POST"])
6476
+ def custom():
6477
+ \"\"\"
6478
+ :request: PayloadSchema
6479
+ :response: PayloadSchema
6480
+ :request-content-type: application/x-www-form-urlencoded
6481
+ :response-content-type: application/problem+json
6482
+ \"\"\"
6483
+ value = request.form.get("value")
6484
+ return {"value": value}, 200
6485
+ """,
6486
+ encoding="utf-8",
6487
+ )
6488
+
6489
+ spec = FlaskASTOpenAPI(tmp_path).generate()
6490
+ operation = spec["paths"]["/custom"]["post"]
6491
+
6492
+ assert "application/x-www-form-urlencoded" in operation["requestBody"]["content"]
6493
+ assert "application/problem+json" in operation["responses"]["200"]["content"]