Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 169 additions & 9 deletions openapi/postprocess_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@
)
parser.add_argument("output_directory", type=Path)
parser.add_argument("package_name")
parser.add_argument("library", choices=("urllib3", "asyncio"))
parser.add_argument(
"namespace",
choices=("kubernetes", "kubernetes.aio", "kubernetes_asyncio"),
)
args = parser.parse_args()

qualified_package_name = f"{args.namespace}.{args.package_name}"

# Swagger 2 exposes these IntOrString fields as free-form objects, which makes
# the generated Pydantic validators reject valid scalar values. Kubernetes
# serializes them as the contained integer or string:
Expand Down Expand Up @@ -91,16 +98,18 @@
if file_type == "markdown":
if path.name == "README.md":
text = re.sub(
r"\A# client(?=\r?\n|\Z)", "# kubernetes.client", text
r"\A# client(?=\r?\n|\Z)",
f"# {qualified_package_name}",
text,
)
text = re.sub(
r"(?<![\w.-])client(?=\.[A-Za-z_])",
"kubernetes.client",
qualified_package_name,
text,
)
text = re.sub(
r"(?m)^(\s*(?:from|import)\s+)client(?=[.\s]|$)",
r"\1kubernetes.client",
rf"\1{qualified_package_name}",
text,
)
for field in int_or_string_fields.get(path.stem.lower(), ()):
Expand All @@ -117,10 +126,15 @@
f"**{field}** | **{new_type}** |",
)
elif args.package_name == "client":
text = text.replace("import client.", "import kubernetes.client.")
text = text.replace("from client", "from kubernetes.client")
text = text.replace(
"getattr(client.models", "getattr(kubernetes.client.models"
"import client.", f"import {qualified_package_name}."
)
text = text.replace(
"from client", f"from {qualified_package_name}"
)
text = text.replace(
"getattr(client.models",
f"getattr({qualified_package_name}.models",
)
# Building every API method's Pydantic schema at import time is
# expensive. defer_build is supported for validate_call since 2.11:
Expand All @@ -129,6 +143,147 @@
"@validate_call\n",
"@validate_call(config={'defer_build': True})\n",
)
if args.library == "asyncio":
if path.name == "configuration.py":
if "import asyncio\n" not in text:
text = text.replace(
"import aiohttp\n", "import asyncio\nimport aiohttp\n"
)
text = text.replace(
" def get_api_key_with_prefix(",
" async def get_api_key_with_prefix(",
)
# Kubernetes token hooks can perform asynchronous refresh:
# https://github.com/kubernetes-client/python/blob/ed9ffd4c3c4bee083c1a585a9559257ea02700eb/scripts/asyncio/client_configuration_async_refresh_api_key_hook.diff
text = text.replace(
Comment thread
tamird marked this conversation as resolved.
Resolved
" if self.refresh_api_key_hook is not None:\n"
" self.refresh_api_key_hook(self)\n",
" if self.refresh_api_key_hook is not None:\n"
" result = self.refresh_api_key_hook(self)\n"
" if asyncio.iscoroutine(result):\n"
" await result\n",
)
text = text.replace(
" def auth_settings(", " async def auth_settings("
)
text = text.replace(
"'value': self.get_api_key_with_prefix(\n",
"'value': await self.get_api_key_with_prefix(\n",
)
if "self.disable_strict_ssl_verification = False\n" not in text:
Comment thread
tamird marked this conversation as resolved.
Resolved
text = text.replace(
" self.verify_ssl = verify_ssl\n",
" self.verify_ssl = verify_ssl\n"
" self.disable_strict_ssl_verification = False\n",
)
elif path.name == "api_client.py":
text = text.replace(
" def param_serialize(",
" async def param_serialize(",
)
text = text.replace(
" self.update_params_for_auth(\n",
" await self.update_params_for_auth(\n",
)
Comment thread
tamird marked this conversation as resolved.
Resolved
text = text.replace(
" def update_params_for_auth(\n",
" async def update_params_for_auth(\n",
)
text = text.replace(
"self.configuration.auth_settings().get(auth)",
"(await self.configuration.auth_settings()).get(auth)",
)
text = text.replace(
" _request_timeout=None\n"
" ) -> rest.RESTResponse:\n",
" _request_timeout=None,\n"
" _preload_content=True,\n"
" ) -> rest.RESTResponse:\n",
)
elif path.name == "rest.py":
if '"read_bufsize": 2**21,' not in text:
Comment thread
tamird marked this conversation as resolved.
Resolved
text = text.replace(
' "trust_env": True,\n',
' "trust_env": True,\n'
" # Kubernetes watch events can exceed "
"aiohttp's default buffer.\n"
' "read_bufsize": 2**21,\n',
)
if "if configuration.disable_strict_ssl_verification:\n" not in text:
text = text.replace(
" self.proxy = configuration.proxy\n",
" if configuration.disable_strict_ssl_verification:\n"
" self.ssl_context.verify_flags "
"&= ~ssl.VERIFY_X509_STRICT\n\n"
" self.proxy = configuration.proxy\n",
)
if 'args["server_hostname"]' not in text:
text = text.replace(
" if self.proxy:\n",
" if self.configuration.tls_server_name:\n"
' args["server_hostname"] = '
"self.configuration.tls_server_name\n\n"
" if self.proxy:\n",
)
if (
"# JSON Patch contains operations; "
"object patches use strategic merge.\n"
) not in text:
text = text.replace(
" # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`\n",
" # JSON Patch contains operations; "
"object patches use strategic merge.\n"
" # https://kubernetes.io/docs/reference/using-api/api-concepts/#updates-to-existing-resources\n"
" if (\n"
" method == 'PATCH'\n"
" and headers['Content-Type'] == "
"'application/json-patch+json'\n"
" and not isinstance(body, list)\n"
" ):\n"
" headers['Content-Type'] = "
"'application/strategic-merge-patch+json'\n\n"
" # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`\n",
)
if (
"headers['Content-Type'] == "
"'application/apply-patch+yaml'"
) not in text:
text = text.replace(
" if re.search('json', headers['Content-Type'], "
"re.IGNORECASE):\n",
" if (\n"
" re.search('json', headers['Content-Type'], "
"re.IGNORECASE)\n"
" or headers['Content-Type'] == "
"'application/apply-patch+yaml'\n"
" ):\n",
)
elif path.parent.name == "api":
text = re.sub(
r"(?m)^ def (_[A-Za-z_][A-Za-z0-9_]*_serialize\()",
r" async def \1",
text,
)
text = text.replace(
" _param = self._",
" _param = await self._",
)
text = text.replace(
" return self.api_client.param_serialize(\n",
" return await self.api_client.param_serialize(\n",
)
# Websocket exec and attach must distinguish buffered calls
# from the generated raw-response operations.
text = re.sub(
r"(?ms)(^ async def "
r"[A-Za-z_][A-Za-z0-9_]*_without_preload_content\("
r".*?^ response_data = "
r"await self\.api_client\.call_api\(\n"
r" \*_param,\n)"
r"(?! _preload_content=False,\n)",
r"\1 _preload_content=False,\n",
text,
)
# Generated files otherwise fail git diff --check on trailing whitespace
# and extra blank lines at EOF.
lines = text.splitlines()
Expand Down Expand Up @@ -172,7 +327,10 @@
)
for line in lines
]
if any(line.startswith(" def patch_") for line in lines):
if any(
line.startswith((" def patch_", " async def patch_"))
for line in lines
):
lines = [
line.replace(
"from pydantic import validate_call, ",
Expand All @@ -182,8 +340,10 @@
]
patch_method = False
for index, line in enumerate(lines):
if line.startswith(" def "):
patch_method = line.startswith(" def patch_")
if line.startswith((" def ", " async def ")):
patch_method = line.startswith(
(" def patch_", " async def patch_")
)
if patch_method:
# Swagger 2 describes PATCH bodies as objects, but RFC 6902
# represents a JSON Patch document as an array of operations:
Expand Down
26 changes: 15 additions & 11 deletions openapi/preprocess_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,22 @@ def fix_python_portforward_ports_parameter(operation, parent):
parameter.pop('format', None)


def fix_python_namespace_delete_response(operation, _):
if operation.get('operationId') != 'deleteCoreV1Namespace':
def fix_python_delete_response(operation, _):
Comment thread
tamird marked this conversation as resolved.
Resolved
if operation.get('x-kubernetes-action') != 'delete':
return
# Namespace storage normally returns the deleted Namespace, but unsafe
# deletion cannot recover the object and returns a Status. Swagger 2
# cannot describe that union, so preserve either response as an object:
# https://github.com/kubernetes/kubernetes/blob/8ba6370120c1371ab70428be16341c3cf6ba8584/pkg/registry/core/namespace/storage/storage.go#L59-L73
# Resource deletion can return the deleted object or Status, but Swagger 2
# cannot describe that union. Preserve either response as an object:
# https://github.com/kubernetes/kubernetes/blob/8ba6370120c1371ab70428be16341c3cf6ba8584/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/corrupt_obj_deleter.go#L104-L127
# https://github.com/kubernetes/kubernetes/blob/8ba6370120c1371ab70428be16341c3cf6ba8584/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/delete.go#L194-L207
for status in ('200', '202'):
response = operation.get('responses', {}).get(status)
if response is not None:
if (
response is not None
and response.get('schema', {}).get('$ref') in {
'#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status',
'#/definitions/v1.Status',
}
):
response['schema'] = {'type': 'object'}


Expand All @@ -216,7 +220,7 @@ def clean_crd_meta(spec):
def add_custom_objects_spec(spec, client_language):
with open(CUSTOM_OBJECTS_SPEC_PATH, 'r') as custom_objects_spec_file:
custom_objects_spec = json.load(custom_objects_spec_file)
if client_language == 'python':
if client_language in ('python', 'python-aio'):
for path_item in custom_objects_spec.values():
patch = path_item.get('patch')
if patch is not None:
Expand Down Expand Up @@ -249,7 +253,7 @@ def add_apidiscovery_definitions(spec):


def add_bearer_token_alias(spec, client_language):
if client_language != 'python':
if client_language not in ('python', 'python-aio'):
return spec
bearer_token = spec.get('securityDefinitions', {}).get('BearerToken')
if bearer_token is not None:
Expand Down Expand Up @@ -322,12 +326,12 @@ def process_swagger(spec, client_language, crd_mode=False):

expand_parameters(spec)

if client_language == 'python':
if client_language in ('python', 'python-aio'):
apply_func_to_spec_operations(spec, fix_exec_command_parameter)
apply_func_to_spec_operations(
spec, fix_python_portforward_ports_parameter)
apply_func_to_spec_operations(
spec, fix_python_namespace_delete_response)
spec, fix_python_delete_response)

apply_func_to_spec_operations(spec, strip_tags_from_operation_id)

Expand Down
4 changes: 3 additions & 1 deletion openapi/python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ echo "--- Post-processing generated code..."

python3 "${SCRIPT_ROOT}/postprocess_python.py" \
"${OUTPUT_DIR}" \
"${PACKAGE_NAME}"
"${PACKAGE_NAME}" \
urllib3 \
kubernetes

echo "---Done."
Loading