Skip to content

Commit d8ca2fa

Browse files
committed
fix: create monkey-patch to force swagger to use multi-part forms
This is a terrible, horrible, no good, very bad patch that I wish I didn't have to do. However, its the only way to get the Swagger behavior we need. It applies the patch in this PR python-restx/flask-restx#542 on the flask-restx repo, however it seems unlikely it will ever be accepted.
1 parent 370ed28 commit d8ca2fa

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

src/dioptra/restapi/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
from .__version__ import __version__ as DIOPTRA_VERSION
4141
from .db import db
42+
from .patches import monkey_patch_flask_restx
4243

4344
LOGGER: BoundLogger = structlog.stdlib.get_logger()
4445

@@ -66,6 +67,8 @@ def create_app(env: Optional[str] = None, injector: Optional[Injector] = None) -
6667
from .routes import register_routes
6768
from .v1.users.service import load_user as v1_load_user
6869

70+
monkey_patch_flask_restx()
71+
6972
if env is None:
7073
env = os.getenv("DIOPTRA_RESTAPI_ENV", "test")
7174

src/dioptra/restapi/patches.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# This Software (Dioptra) is being made available as a public service by the
2+
# National Institute of Standards and Technology (NIST), an Agency of the United
3+
# States Department of Commerce. This software was developed in part by employees of
4+
# NIST and in part by NIST contractors. Copyright in portions of this software that
5+
# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant
6+
# to Title 17 United States Code Section 105, works of NIST employees are not
7+
# subject to copyright protection in the United States. However, NIST may hold
8+
# international copyright in software created by its employees and domestic
9+
# copyright (or licensing rights) in portions of software that were assigned or
10+
# licensed to NIST. To the extent that NIST holds copyright in this software, it is
11+
# being made available under the Creative Commons Attribution 4.0 International
12+
# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts
13+
# of the software developed or licensed by NIST.
14+
#
15+
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
16+
# https://creativecommons.org/licenses/by/4.0/legalcode
17+
import hashlib
18+
import inspect
19+
from typing import Any
20+
21+
EXPECTED_SERIALIZE_OPERATION_SHA256_HASH = "57241f0a33ed5e1771e5032d1e6f6994685185ed526b9ca2c70f4f27684d1f92" # noqa: B950; fmt: skip
22+
23+
24+
def monkey_patch_flask_restx() -> None:
25+
"""
26+
Monkey patch flask_restx.Swagger.serialize_operation to force Swagger docs to use
27+
the multipart/form-data content type for multi-file uploads instead of the
28+
application/x-www-form-urlencoded content type.
29+
30+
This monkey-patch applies the proposed change in this PR
31+
https://github.com/python-restx/flask-restx/pull/542.
32+
"""
33+
import flask_restx
34+
from flask_restx.utils import not_none
35+
36+
serialize_operation_sha256_hash = get_source_code_hash(
37+
flask_restx.Swagger.serialize_operation
38+
)
39+
40+
if serialize_operation_sha256_hash != EXPECTED_SERIALIZE_OPERATION_SHA256_HASH:
41+
raise RuntimeError(
42+
"Source code hash changed (reason: hash of "
43+
"flask_restx.Swagger.serialize_operation did not match "
44+
f"{EXPECTED_SERIALIZE_OPERATION_SHA256_HASH}): "
45+
f"{serialize_operation_sha256_hash}"
46+
)
47+
48+
def serialize_operation_patched(self, doc, method):
49+
operation = {
50+
"responses": self.responses_for(doc, method) or None,
51+
"summary": doc[method]["docstring"]["summary"],
52+
"description": self.description_for(doc, method) or None,
53+
"operationId": self.operation_id_for(doc, method),
54+
"parameters": self.parameters_for(doc[method]) or None,
55+
"security": self.security_for(doc, method),
56+
}
57+
# Handle 'produces' mimetypes documentation
58+
if "produces" in doc[method]:
59+
operation["produces"] = doc[method]["produces"]
60+
# Handle deprecated annotation
61+
if doc.get("deprecated") or doc[method].get("deprecated"):
62+
operation["deprecated"] = True
63+
# Handle form exceptions:
64+
doc_params = list(doc.get("params", {}).values())
65+
all_params = doc_params + (operation["parameters"] or [])
66+
if all_params and any(p["in"] == "formData" for p in all_params):
67+
if any(p["type"] == "file" for p in all_params):
68+
operation["consumes"] = ["multipart/form-data"]
69+
elif any(
70+
p["type"] == "array" and p["collectionFormat"] == "multi"
71+
for p in all_params
72+
if "collectionFormat" in p
73+
):
74+
operation["consumes"] = ["multipart/form-data"]
75+
else:
76+
operation["consumes"] = [
77+
"application/x-www-form-urlencoded",
78+
"multipart/form-data",
79+
]
80+
operation.update(self.vendor_fields(doc, method))
81+
return not_none(operation)
82+
83+
flask_restx.Swagger.serialize_operation = serialize_operation_patched
84+
85+
86+
def get_source_code_hash(obj: Any) -> str:
87+
"""Generate a hash of the underlying source code of a Python object.
88+
89+
Args:
90+
obj: The Python object for which to generate a source code hash.
91+
92+
Returns:
93+
The hash of the source code of the Python object.
94+
"""
95+
96+
hash_sha256 = hashlib.sha256()
97+
source_lines, _ = inspect.getsourcelines(obj)
98+
source_lines = [line.rstrip() for line in source_lines]
99+
100+
for line in source_lines:
101+
hash_sha256.update(line.encode("utf-8"))
102+
103+
return hash_sha256.hexdigest()

0 commit comments

Comments
 (0)