pypa/gh-action-pypi-publish
(pypa/gh-action-pypi-publish)
###
[`v1.14.2`](https://redirect.github.com/pypa/gh-action-pypi-publish/releases/tag/v1.14.2)
[Compare
Source](https://redirect.github.com/pypa/gh-action-pypi-publish/compare/v1.14.1...v1.14.2)
This one probably won't touch you visibly so just
bookmark @webknjaz's
EuroPython 2026 “AI” slop rant for when it's published on YouTube or
encourage him to
come back with more to share next year!
#### 🛠️ Urgh… Another release!? Again? Explain yourself!
Looking at the diff, you'll only witness updates across the dependency
tree. That's it! It's not a security fix or anything like that even, no.
But you'll want this update.
> \[!tip]
> So what *most* people will find useful is
[@takluyver](https://redirect.github.com/takluyver)[💰](https://redirect.github.com/sponsors/takluyver)'s
update of Twine to v7 that we use internally
([#416](https://redirect.github.com/pypa/gh-action-pypi-publish/issues/416)).
This version will let them upload their sdists and wheels containing
core packaging metadata v2.5 to (Test)PyPI.
#### 🧐 Tell me why..
The rest of the updates bump things related to
pypi-attestations and sigstore, which has the
most interesting backstory here. @facutuesca💰 sent a
patch in #417 but a bunch more helped out.
TL;DR non-pure-python projects with C-extensions tend to have dozens
(sometimes hundreds) wheels to upload to PyPI per release. They are
often quite big and take time to transfer over the network. People
started noticing problems and coming up with DIY sharding workarounds
like
[aio-libs/aiohttp#13226](https://redirect.github.com/aio-libs/aiohttp/pull/13226)
around July 23.
On this date, projects with a good amount of bytes to publish would
start getting timeouts 5 minutes after the PyPI publishing job begun.
The same job that worked just fine before.
I had to start pinging upstream library and ecosystem people, on GitHub
and privately, to start making sense of what was happening. Eventually,
we collectively concluded that GitHub must've shortened the lifetime of
their OIDC identity — it seems to have used to be 10 minutes long (at
some point in the past) and is now 5 minutes, apparently. It's not
documented clearly, and we have not been able to get any clarity by
attempting to contact GitHub through private channels, using personal
connections.
Over the course of investigation,
[@facutuesca](https://redirect.github.com/facutuesca)[💰](https://redirect.github.com/sponsors/facutuesca)
found and fixed a related underlying cache invalidation bug in
[sigstore/sigstore-python#1838](https://redirect.github.com/sigstore/sigstore-python/pull/1838),
which he then coordinated propagation through the dependency chain
updates in sigstore-python, pypi-attestations, gh-action-pypi-publish
and gh-action-sigstore-python.
Mike's also discovered that Sigstore's Rekor slowdown seems to have
become the main contributing cause of the last week's incident. He's
collected some data to support this claim:
.
#### 🫶 New Contributors
- [@davidbrochart](https://redirect.github.com/davidbrochart)
made their first contribution in
[#415](https://redirect.github.com/pypa/gh-action-pypi-publish/issues/415)
- [@takluyver](https://redirect.github.com/takluyver) made their
first contribution in
[#416](https://redirect.github.com/pypa/gh-action-pypi-publish/issues/416)
**🪞 Full Diff**:
**🧔♂️ Release Manager:**
[@webknjaz](https://redirect.github.com/sponsors/webknjaz)
[🇺🇦](https://stand-with-ukraine.pp.ua)
**🙏 Special Thanks** to
[@davidbrochart](https://redirect.github.com/davidbrochart)[💰](https://redirect.github.com/sponsors/davidbrochart)
and
[@Dreamsorcerer](https://redirect.github.com/Dreamsorcerer)[💰](https://redirect.github.com/sponsors/Dreamsorcerer)
for turning my attention (in
[#415](https://redirect.github.com/pypa/gh-action-pypi-publish/issues/415)
and in private) to the newly surfaced corner case in GitHub's behavior
that only affected a narrow category of projects while many others
remained blissfully unaware.
[@bdraco](https://redirect.github.com/bdraco)[💰](https://redirect.github.com/sponsors/bdraco)
came up with a DIY sharding workaround for aiohttp that served as a demo
for other projects.
[@miketheman](https://redirect.github.com/miketheman)[💰](https://redirect.github.com/sponsors/miketheman)
confirmed the Warehouse-side details. Also,
[@jku](https://redirect.github.com/jku)[💰](https://redirect.github.com/sponsors/jku)
and
[@woodruffw](https://redirect.github.com/woodruffw)[💰](https://redirect.github.com/sponsors/woodruffw)
helped work through, review and release the Sigstore ecosystem upstream
libs.
**💬 Discuss** [on Bluesky
🦋](https://bsky.app/profile/did:plc:ve6s3mxkefjaxty3m4fdqumn/post/3mrsqy2xba22j),
[on Mastodon
🐘](https://mastodon.social/@webknjaz/117005132816750073) and [on
GitHub][release discussion].
[![GH Sponsors badge]][GH Sponsors URL]
[GH Sponsors badge]:
https://img.shields.io/badge/%40webknjaz-transparent?logo=githubsponsors&logoColor=%23EA4AAA&label=Sponsor&color=2a313c
[GH Sponsors URL]: https://redirect.github.com/sponsors/webknjaz
[release discussion]:
https://redirect.github.com/pypa/gh-action-pypi-publish/discussions/419
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/openapi-generators/openapi-python-client).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/release.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4d21d582a..163bb4ab5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,6 +18,6 @@ jobs:
- name: Build
run: hatchling build
- name: Push to PyPI
- uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
attestations: true
From 1c99af478892e5bbe6583b92acba431c9dec9186 Mon Sep 17 00:00:00 2001
From: Dylan Anthony <43723790+dbanty@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:30:04 -0600
Subject: [PATCH 62/63] Comprehensive string handling rewrite (#1483)
Prevents OpenAPI Document content from escaping strings, docstrings, and
f-strings in generated code by systematically rewriting all string
handling. Solves
https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-5293-mq8x-g3xj
---
...arbitrary_code_generation_vulnerability.md | 13 +
...eaking_changes_for_all_custom_templates.md | 22 ++
...tifiers_when_resolving_naming_conflicts.md | 7 +
...cters_now_stripped_from_string_literals.md | 11 +
.changeset/render_error_data_as_json.md | 7 +
...cstrings_for_models_with_no_description.md | 5 +
.github/workflows/fuzz.yml | 33 ++
.gitignore | 6 +-
end_to_end_tests/__init__.py | 3 +-
.../__snapshots__/test_end_to_end.ambr | 12 +-
end_to_end_tests/escapes.openapi.yaml | 179 +++++++++
end_to_end_tests/functional_tests/README.md | 10 +-
.../generated_code_execution/test_arrays.py | 10 +-
.../generated_code_execution/test_defaults.py | 13 +-
.../test_docstrings.py | 20 +-
.../test_enums_and_consts.py | 55 +--
.../test_path_parameters.py | 6 +-
.../test_properties.py | 26 +-
.../generated_code_execution/test_unions.py | 17 +-
.../test_invalid_arrays.py | 6 +-
.../test_invalid_defaults.py | 4 +-
.../test_invalid_enums_and_consts.py | 22 +-
.../test_invalid_references.py | 4 +-
.../test_invalid_spec_format.py | 13 +-
.../test_invalid_unions.py | 4 +-
end_to_end_tests/functional_tests/helpers.py | 39 +-
end_to_end_tests/generated_client.py | 46 +--
.../custom-templates}/README.md | 0
.../my_test_api_client/api/__init__.py | 0
.../my_test_api_client/api/bodies/__init__.py | 0
.../my_test_api_client/api/config/__init__.py | 0
.../api/default/__init__.py | 0
.../api/defaults/__init__.py | 0
.../my_test_api_client/api/enums/__init__.py | 0
.../api/location/__init__.py | 0
.../my_test_api_client/api/naming/__init__.py | 0
.../api/parameter_references/__init__.py | 0
.../api/parameters/__init__.py | 0
.../api/responses/__init__.py | 0
.../my_test_api_client/api/tag1/__init__.py | 0
.../my_test_api_client/api/tag2/__init__.py | 0
.../my_test_api_client/api/tests/__init__.py | 2 +-
.../my_test_api_client/api/true_/__init__.py | 0
.../my_test_api_client/models/__init__.py | 0
.../.gitignore | 0
.../README.md | 0
.../my_test_api_client/__init__.py | 0
.../my_test_api_client/api/__init__.py | 0
.../my_test_api_client/client.py | 0
.../my_test_api_client/errors.py | 0
.../my_test_api_client/models/__init__.py | 0
.../models/model_with_description.py | 0
.../models/model_with_no_description.py | 0
.../my_test_api_client/py.typed | 0
.../my_test_api_client/types.py | 0
.../pyproject.toml | 0
.../escapes-client}/.gitignore | 0
.../golden-records/escapes-client/README.md | 124 ++++++
.../escapes-client/escapes_client/__init__.py | 8 +
.../escapes_client}/api/__init__.py | 0
.../escapes_client/api/default}/__init__.py | 0
.../api/default/get_weird_responses.py | 76 ++++
.../api/default/misc_metadata_escapes.py | 150 ++++++++
.../api/default/non_string_example.py | 101 +++++
.../api/default/property_escapes.py | 101 +++++
.../api/printtag_escape}/__init__.py | 0
.../api/printtag_escape/with_braces_path.py | 131 +++++++
.../escapes-client/escapes_client}/client.py | 0
.../escapes-client/escapes_client}/errors.py | 0
.../escapes_client/models/__init__.py | 19 +
.../models/misc_metadata_escapes_body.py | 45 +++
.../models/non_string_example_body.py | 75 ++++
.../non_string_example_body_dict_example.py | 51 +++
...property_escapes_model_title_printuh_oh.py | 92 +++++
...pes_model_title_printuh_oh_escaped_enum.py | 9 +
.../models/schema_printuh_oh.py | 45 +++
.../models/with_braces_path_body.py | 45 +++
.../escapes-client/escapes_client}/py.typed | 0
.../escapes-client/escapes_client}/types.py | 0
.../escapes-client/pyproject.toml | 25 ++
.../my-enum-api-client}/.gitignore | 0
.../my-enum-api-client}/README.md | 0
.../my_enum_api_client/__init__.py | 0
.../my_enum_api_client/api/__init__.py | 0
.../my_enum_api_client/api/enums}/__init__.py | 0
.../enums/bool_enum_tests_bool_enum_post.py | 0
.../api/enums/int_enum_tests_int_enum_post.py | 0
.../my_enum_api_client/api/tests}/__init__.py | 0
.../api/tests/get_user_list.py | 0
.../api/tests/post_user_list.py | 0
.../my_enum_api_client/client.py | 0
.../my_enum_api_client/errors.py | 0
.../my_enum_api_client/models/__init__.py | 0
.../my_enum_api_client/models/a_model.py | 0
.../models/an_all_of_enum.py | 0
.../my_enum_api_client/models/an_enum.py | 0
.../models/an_enum_with_null.py | 0
.../my_enum_api_client/models/an_int_enum.py | 0
.../models/different_enum.py | 0
.../models/get_user_list_int_enum_header.py | 0
.../get_user_list_string_enum_header.py | 0
.../models/post_user_list_body.py | 0
.../my_enum_api_client/py.typed | 0
.../my_enum_api_client/types.py | 0
.../my-enum-api-client}/pyproject.toml | 0
.../my-test-api-client}/.gitignore | 0
.../my-test-api-client}/README.md | 0
.../my_test_api_client/__init__.py | 0
.../my_test_api_client}/api/__init__.py | 0
.../api/bodies}/__init__.py | 0
.../api/bodies/json_like.py | 0
.../api/bodies/optional_body.py | 0
.../api/bodies/post_bodies_multiple.py | 0
.../my_test_api_client/api/bodies/refs.py | 0
.../api/config}/__init__.py | 0
.../api/config/content_type_override.py | 0
.../api/default}/__init__.py | 0
.../api/default/get_common_parameters.py | 0
.../api/default/get_models_allof.py | 0
.../get_models_oneof_with_required_const.py | 0
.../api/default/post_common_parameters.py | 0
.../post_types_unions_duplicate_types.py | 0
.../api/default/reserved_parameters.py | 0
.../api/defaults}/__init__.py | 0
.../defaults/defaults_tests_defaults_post.py | 0
.../my_test_api_client/api/enums}/__init__.py | 0
.../enums/bool_enum_tests_bool_enum_post.py | 0
.../api/enums/int_enum_tests_int_enum_post.py | 0
.../api/location}/__init__.py | 0
.../api/location/get_location_header_types.py | 0
.../get_location_query_optionality.py | 0
.../api/naming}/__init__.py | 0
.../api/naming/hyphen_in_path.py | 0
.../api/naming/mixed_case.py | 0
...st_naming_property_conflict_with_import.py | 0
.../api/parameter_references}/__init__.py | 0
.../get_parameter_references_path_param.py | 0
.../api/parameters}/__init__.py | 0
...lete_common_parameters_overriding_param.py | 0
.../get_common_parameters_overriding_param.py | 0
.../get_same_name_multiple_locations_param.py | 0
.../parameters/multiple_path_parameters.py | 0
.../api/responses}/__init__.py | 0
.../api/responses/default_status_code.py | 0
..._responses_unions_simple_before_complex.py | 0
.../api/responses/reference_response.py | 0
.../api/responses/status_code_patterns.py | 0
.../api/responses/status_code_precedence.py | 0
.../api/responses/text_response.py | 0
.../my_test_api_client/api/tag1}/__init__.py | 0
.../api/tag1/get_tag_with_number.py | 0
.../my_test_api_client/api/tag2}/__init__.py | 0
.../api/tag2/get_tag_with_number.py | 0
.../my_test_api_client/api/tests}/__init__.py | 0
.../api/tests/callback_test.py | 0
.../api/tests/description_with_backslash.py | 8 +-
.../api/tests/get_basic_list_of_booleans.py | 0
.../api/tests/get_basic_list_of_floats.py | 0
.../api/tests/get_basic_list_of_integers.py | 0
.../api/tests/get_basic_list_of_strings.py | 0
.../api/tests/get_user_list.py | 0
.../tests/json_body_tests_json_body_post.py | 0
.../no_response_tests_no_response_get.py | 0
.../octet_stream_tests_octet_stream_get.py | 0
.../octet_stream_tests_octet_stream_post.py | 0
.../api/tests/post_form_data.py | 0
.../api/tests/post_form_data_inline.py | 0
.../api/tests/post_tests_json_body_string.py | 0
.../api/tests/test_inline_objects.py | 0
..._with_cookie_auth_token_with_cookie_get.py | 0
...d_content_tests_unsupported_content_get.py | 0
.../tests/upload_file_tests_upload_post.py | 0
.../my_test_api_client/api/true_}/__init__.py | 0
.../my_test_api_client/api/true_/false_.py | 0
.../my_test_api_client}/client.py | 0
.../my_test_api_client}/errors.py | 0
.../my_test_api_client/models/__init__.py | 0
.../models/a_discriminated_union_type_1.py | 0
.../models/a_discriminated_union_type_2.py | 0
.../my_test_api_client/models/a_form_data.py | 0
.../my_test_api_client/models/a_model.py | 0
...roperties_reference_that_are_not_object.py | 0
.../all_of_has_properties_but_no_type.py | 0
...of_has_properties_but_no_type_type_enum.py | 0
.../models/all_of_required_base.py | 0
.../models/all_of_required_derived.py | 0
.../models/all_of_sub_model.py | 0
.../models/all_of_sub_model_type_enum.py | 0
.../models/an_all_of_enum.py | 0
...h_a_circular_ref_in_items_object_a_item.py | 0
...ems_object_additional_properties_a_item.py | 2 -
...ems_object_additional_properties_b_item.py | 2 -
...h_a_circular_ref_in_items_object_b_item.py | 0
...items_object_additional_properties_item.py | 2 -
...th_a_recursive_ref_in_items_object_item.py | 0
.../my_test_api_client/models/an_enum.py | 0
.../models/an_enum_with_null.py | 0
.../my_test_api_client/models/an_int_enum.py | 0
.../models/another_all_of_sub_model.py | 0
.../models/another_all_of_sub_model_type.py | 0
.../another_all_of_sub_model_type_enum.py | 0
.../body_upload_file_tests_upload_post.py | 0
...e_tests_upload_post_additional_property.py | 0
..._tests_upload_post_some_nullable_object.py | 0
...load_file_tests_upload_post_some_object.py | 0
..._tests_upload_post_some_optional_object.py | 0
.../models/different_enum.py | 0
.../my_test_api_client/models/extended.py | 0
.../models/free_form_model.py | 2 -
...t_location_header_types_int_enum_header.py | 0
...ocation_header_types_string_enum_header.py | 0
.../models/get_models_allof_response_200.py | 0
...with_required_const_response_200_type_0.py | 0
...with_required_const_response_200_type_1.py | 0
.../models/http_validation_error.py | 0
.../my_test_api_client/models/import_.py | 2 -
.../models/json_like_body.py | 0
.../models/mixed_case_response_200.py | 0
.../models/model_from_all_of.py | 0
.../my_test_api_client/models/model_name.py | 2 -
.../models/model_reference_with_periods.py | 0
...odel_with_additional_properties_inlined.py | 0
..._properties_inlined_additional_property.py | 0
.../model_with_additional_properties_refed.py | 2 -
.../models/model_with_any_json_properties.py | 2 -
...n_properties_additional_property_type_0.py | 2 -
.../model_with_backslash_in_description.py | 2 +-
.../models/model_with_circular_ref_a.py | 0
.../models/model_with_circular_ref_b.py | 0
...circular_ref_in_additional_properties_a.py | 2 -
...circular_ref_in_additional_properties_b.py | 2 -
.../models/model_with_date_time_property.py | 0
.../models/model_with_discriminated_union.py | 0
.../models/model_with_merged_properties.py | 0
...l_with_merged_properties_string_to_enum.py | 0
.../models/model_with_no_properties.py | 2 -
...el_with_primitive_additional_properties.py | 0
...ive_additional_properties_a_date_holder.py | 2 -
.../models/model_with_property_ref.py | 0
.../models/model_with_recursive_ref.py | 0
..._recursive_ref_in_additional_properties.py | 2 -
.../models/model_with_union_property.py | 0
.../model_with_union_property_inlined.py | 0
...odel_with_union_property_inlined_apples.py | 0
...del_with_union_property_inlined_bananas.py | 0
.../my_test_api_client/models/none.py | 2 -
...am_tests_octet_stream_post_response_200.py | 0
.../models/optional_body_body.py | 2 -
.../models/post_bodies_multiple_data_body.py | 0
.../models/post_bodies_multiple_files_body.py | 0
.../models/post_bodies_multiple_json_body.py | 0
.../models/post_form_data_inline_body.py | 0
...ming_property_conflict_with_import_body.py | 0
...perty_conflict_with_import_response_200.py | 0
...ions_simple_before_complex_response_200.py | 0
...ple_before_complex_response_200a_type_1.py | 2 -
.../status_code_patterns_response_2xx.py | 0
...tatus_code_patterns_response_2xx_status.py | 0
.../status_code_patterns_response_4xx.py | 0
.../models/test_inline_objects_body.py | 0
.../test_inline_objects_response_200.py | 0
.../models/validation_error.py | 0
.../my_test_api_client}/py.typed | 0
.../my_test_api_client}/types.py | 0
.../my-test-api-client}/pyproject.toml | 0
.../test-3-1-features-client/.gitignore | 23 ++
.../test-3-1-features-client}/README.md | 0
.../test-3-1-features-client}/pyproject.toml | 0
.../test_3_1_features_client/__init__.py | 0
.../test_3_1_features_client/api/__init__.py | 1 +
.../api/const/__init__.py | 1 +
.../api/const/post_const_path.py | 0
.../api/prefix_items/__init__.py | 1 +
.../api/prefix_items/post_prefix_items.py | 0
.../test_3_1_features_client/client.py | 268 +++++++++++++
.../test_3_1_features_client/errors.py | 16 +
.../models/__init__.py | 0
.../models/post_const_path_body.py | 0
.../models/post_prefix_items_body.py | 0
.../test_3_1_features_client/py.typed | 1 +
.../test_3_1_features_client/types.py | 54 +++
end_to_end_tests/regen_golden_record.py | 54 ++-
.../endpoint_init.py.jinja | 4 +-
end_to_end_tests/test_end_to_end.py | 75 ++--
fuzz/test_codegen.py | 349 +++++++++++++++++
knope.toml | 6 +-
openapi_python_client/__init__.py | 43 ++-
openapi_python_client/cli.py | 3 +-
openapi_python_client/config.py | 4 +-
openapi_python_client/parser/bodies.py | 15 +-
openapi_python_client/parser/openapi.py | 106 +++---
.../parser/properties/__init__.py | 67 ++--
.../parser/properties/any.py | 21 +-
.../parser/properties/boolean.py | 26 +-
.../parser/properties/const.py | 21 +-
.../parser/properties/date.py | 23 +-
.../parser/properties/datetime.py | 23 +-
.../parser/properties/enum_property.py | 43 ++-
.../parser/properties/file.py | 21 +-
.../parser/properties/float.py | 26 +-
.../parser/properties/int.py | 22 +-
.../parser/properties/list_property.py | 44 +--
.../properties/literal_enum_property.py | 40 +-
.../parser/properties/merge_properties.py | 15 +-
.../parser/properties/model_property.py | 67 ++--
.../parser/properties/none.py | 22 +-
.../parser/properties/protocol.py | 74 ++--
.../parser/properties/schemas.py | 53 +--
.../parser/properties/string.py | 21 +-
.../parser/properties/union.py | 46 +--
.../parser/properties/uuid.py | 22 +-
openapi_python_client/parser/responses.py | 53 +--
openapi_python_client/schema/__init__.py | 3 +-
.../openapi_schema_pydantic/callback.py | 4 +-
.../openapi_schema_pydantic/components.py | 19 +-
.../schema/openapi_schema_pydantic/contact.py | 8 +-
.../openapi_schema_pydantic/discriminator.py | 6 +-
.../openapi_schema_pydantic/encoding.py | 7 +-
.../schema/openapi_schema_pydantic/example.py | 8 +-
.../external_documentation.py | 6 +-
.../schema/openapi_schema_pydantic/header.py | 26 +-
.../schema/openapi_schema_pydantic/info.py | 9 +-
.../schema/openapi_schema_pydantic/license.py | 6 +-
.../schema/openapi_schema_pydantic/link.py | 9 +-
.../openapi_schema_pydantic/media_type.py | 5 +-
.../openapi_schema_pydantic/oauth_flow.py | 10 +-
.../openapi_schema_pydantic/open_api.py | 8 +-
.../openapi_schema_pydantic/operation.py | 11 +-
.../openapi_schema_pydantic/parameter.py | 11 +-
.../openapi_schema_pydantic/path_item.py | 8 +-
.../schema/openapi_schema_pydantic/paths.py | 3 +-
.../openapi_schema_pydantic/reference.py | 4 +-
.../openapi_schema_pydantic/request_body.py | 5 +-
.../openapi_schema_pydantic/response.py | 9 +-
.../openapi_schema_pydantic/responses.py | 3 +-
.../schema/openapi_schema_pydantic/schema.py | 13 +-
.../security_requirement.py | 4 +-
.../security_scheme.py | 15 +-
.../schema/openapi_schema_pydantic/server.py | 7 +-
.../server_variable.py | 8 +-
.../schema/openapi_schema_pydantic/tag.py | 5 +-
.../schema/openapi_schema_pydantic/xml.py | 8 +-
openapi_python_client/schema/ref.py | 52 +++
.../schema/untrusted_string.py | 56 +++
openapi_python_client/strings.py | 245 ++++++++++++
.../templates/README.md.jinja | 2 +-
.../templates/endpoint_macros.py.jinja | 36 +-
.../templates/endpoint_module.py.jinja | 26 +-
openapi_python_client/templates/helpers.jinja | 10 +-
.../templates/literal_enum.py.jinja | 2 +-
.../templates/model.py.jinja | 28 +-
.../templates/package_init.py.jinja | 4 +-
.../const_property.py.jinja | 6 +-
.../property_templates/date_property.py.jinja | 2 +-
.../datetime_property.py.jinja | 2 +-
.../property_templates/enum_property.py.jinja | 2 +-
.../property_templates/file_property.py.jinja | 2 +-
.../property_templates/list_property.py.jinja | 6 +-
.../literal_enum_property.py.jinja | 4 +-
.../model_property.py.jinja | 2 +-
.../property_macros.py.jinja | 2 +-
.../union_property.py.jinja | 18 +-
.../property_templates/uuid_property.py.jinja | 2 +-
.../templates/pyproject_pdm.toml.jinja | 4 +-
.../templates/pyproject_poetry.toml.jinja | 4 +-
.../templates/pyproject_uv.toml.jinja | 4 +-
.../templates/setup.py.jinja | 4 +-
openapi_python_client/utils.py | 122 ------
pdm.lock | 99 ++++-
pdm.minimal.lock | 286 ++++++++------
pyproject.toml | 35 +-
tests/conftest.py | 16 +-
tests/test___init__.py | 40 +-
tests/test_config.py | 6 +-
tests/test_parser/test_bodies.py | 27 +-
tests/test_parser/test_openapi.py | 355 +++---------------
.../test_properties/test_enum_property.py | 27 ++
.../test_parser/test_properties/test_init.py | 303 +++------------
.../test_properties/test_merge_properties.py | 35 +-
.../test_properties/test_model_property.py | 75 +++-
.../test_parser/test_properties/test_none.py | 2 +-
.../test_properties/test_protocol.py | 73 +++-
.../test_properties/test_schemas.py | 55 +--
.../test_parser/test_properties/test_union.py | 7 +-
tests/test_parser/test_responses.py | 104 +----
tests/test_schema/test_open_api.py | 3 +-
tests/test_schema/test_untrusted_string.py | 31 ++
tests/test_strings.py | 328 ++++++++++++++++
tests/test_templates/conftest.py | 3 +-
tests/test_templates/test_endpoint_macros.py | 36 ++
tests/test_templates/test_model_template.py | 8 +-
.../test_uuid_property/test_uuid_multipart.py | 2 +-
tests/test_utils.py | 136 -------
393 files changed, 4477 insertions(+), 2020 deletions(-)
create mode 100644 .changeset/arbitrary_code_generation_vulnerability.md
create mode 100644 .changeset/breaking_changes_for_all_custom_templates.md
create mode 100644 .changeset/fixed_invalid_identifiers_when_resolving_naming_conflicts.md
create mode 100644 .changeset/many_control_characters_now_stripped_from_string_literals.md
create mode 100644 .changeset/render_error_data_as_json.md
create mode 100644 .changeset/stopped_generating_empty_docstrings_for_models_with_no_description.md
create mode 100644 .github/workflows/fuzz.yml
create mode 100644 end_to_end_tests/escapes.openapi.yaml
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/README.md (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/bodies/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/config/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/default/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/defaults/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/enums/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/location/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/naming/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/parameter_references/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/parameters/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/responses/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/tag1/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/tag2/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/tests/__init__.py (99%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/api/true_/__init__.py (100%)
rename end_to_end_tests/{custom-templates-golden-record => golden-records/custom-templates}/my_test_api_client/models/__init__.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/.gitignore (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/README.md (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/client.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/errors.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/py.typed (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/my_test_api_client/types.py (100%)
rename end_to_end_tests/{ => golden-records}/docstrings-on-attributes-golden-record/pyproject.toml (100%)
rename end_to_end_tests/{golden-record => golden-records/escapes-client}/.gitignore (100%)
create mode 100644 end_to_end_tests/golden-records/escapes-client/README.md
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py
rename end_to_end_tests/{golden-record/my_test_api_client => golden-records/escapes-client/escapes_client}/api/__init__.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/bodies => golden-records/escapes-client/escapes_client/api/default}/__init__.py (100%)
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py
rename end_to_end_tests/{golden-record/my_test_api_client/api/config => golden-records/escapes-client/escapes_client/api/printtag_escape}/__init__.py (100%)
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py
rename end_to_end_tests/{golden-record/my_test_api_client => golden-records/escapes-client/escapes_client}/client.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client => golden-records/escapes-client/escapes_client}/errors.py (100%)
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py
create mode 100644 end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py
rename end_to_end_tests/{golden-record/my_test_api_client => golden-records/escapes-client/escapes_client}/py.typed (100%)
rename end_to_end_tests/{golden-record/my_test_api_client => golden-records/escapes-client/escapes_client}/types.py (100%)
create mode 100644 end_to_end_tests/golden-records/escapes-client/pyproject.toml
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/.gitignore (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/README.md (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/__init__.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/api/__init__.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/default => golden-records/my-enum-api-client/my_enum_api_client/api/enums}/__init__.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client => golden-records/my-enum-api-client/my_enum_api_client}/api/enums/bool_enum_tests_bool_enum_post.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/defaults => golden-records/my-enum-api-client/my_enum_api_client/api/tests}/__init__.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/api/tests/get_user_list.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/api/tests/post_user_list.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/client.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/errors.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/__init__.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/a_model.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/an_all_of_enum.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/an_enum.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/an_enum_with_null.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/an_int_enum.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/different_enum.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/get_user_list_int_enum_header.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/get_user_list_string_enum_header.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/models/post_user_list_body.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/py.typed (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/my_enum_api_client/types.py (100%)
rename end_to_end_tests/{literal-enums-golden-record => golden-records/my-enum-api-client}/pyproject.toml (100%)
rename end_to_end_tests/{test-3-1-golden-record => golden-records/my-test-api-client}/.gitignore (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/README.md (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/__init__.py (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client => golden-records/my-test-api-client/my_test_api_client}/api/__init__.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/enums => golden-records/my-test-api-client/my_test_api_client/api/bodies}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/bodies/json_like.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/bodies/optional_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/bodies/post_bodies_multiple.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/bodies/refs.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/location => golden-records/my-test-api-client/my_test_api_client/api/config}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/config/content_type_override.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/naming => golden-records/my-test-api-client/my_test_api_client/api/default}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/default/get_common_parameters.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/default/get_models_allof.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/default/get_models_oneof_with_required_const.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/default/post_common_parameters.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/default/post_types_unions_duplicate_types.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/default/reserved_parameters.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/parameter_references => golden-records/my-test-api-client/my_test_api_client/api/defaults}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/defaults/defaults_tests_defaults_post.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/parameters => golden-records/my-test-api-client/my_test_api_client/api/enums}/__init__.py (100%)
rename end_to_end_tests/{literal-enums-golden-record/my_enum_api_client => golden-records/my-test-api-client/my_test_api_client}/api/enums/bool_enum_tests_bool_enum_post.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/responses => golden-records/my-test-api-client/my_test_api_client/api/location}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/location/get_location_header_types.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/location/get_location_query_optionality.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/tag1 => golden-records/my-test-api-client/my_test_api_client/api/naming}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/naming/hyphen_in_path.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/naming/mixed_case.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/tag2 => golden-records/my-test-api-client/my_test_api_client/api/parameter_references}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/tests => golden-records/my-test-api-client/my_test_api_client/api/parameters}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/parameters/multiple_path_parameters.py (100%)
rename end_to_end_tests/{golden-record/my_test_api_client/api/true_ => golden-records/my-test-api-client/my_test_api_client/api/responses}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/responses/default_status_code.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/responses/reference_response.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/responses/status_code_patterns.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/responses/status_code_precedence.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/responses/text_response.py (100%)
rename end_to_end_tests/{literal-enums-golden-record/my_enum_api_client/api/enums => golden-records/my-test-api-client/my_test_api_client/api/tag1}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tag1/get_tag_with_number.py (100%)
rename end_to_end_tests/{literal-enums-golden-record/my_enum_api_client/api/tests => golden-records/my-test-api-client/my_test_api_client/api/tag2}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tag2/get_tag_with_number.py (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client/api/const => golden-records/my-test-api-client/my_test_api_client/api/tests}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/callback_test.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/description_with_backslash.py (94%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/get_basic_list_of_booleans.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/get_basic_list_of_floats.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/get_basic_list_of_integers.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/get_basic_list_of_strings.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/get_user_list.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/json_body_tests_json_body_post.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/no_response_tests_no_response_get.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/post_form_data.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/post_form_data_inline.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/post_tests_json_body_string.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/test_inline_objects.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/tests/upload_file_tests_upload_post.py (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client/api/prefix_items => golden-records/my-test-api-client/my_test_api_client/api/true_}/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/api/true_/false_.py (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client => golden-records/my-test-api-client/my_test_api_client}/client.py (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client => golden-records/my-test-api-client/my_test_api_client}/errors.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/__init__.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/a_discriminated_union_type_1.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/a_discriminated_union_type_2.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/a_form_data.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/a_model.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/all_of_has_properties_but_no_type.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/all_of_required_base.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/all_of_required_derived.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/all_of_sub_model.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/all_of_sub_model_type_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_all_of_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_enum_with_null.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/an_int_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/another_all_of_sub_model.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/another_all_of_sub_model_type.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/another_all_of_sub_model_type_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/body_upload_file_tests_upload_post.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/different_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/extended.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/free_form_model.py (98%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/get_location_header_types_int_enum_header.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/get_location_header_types_string_enum_header.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/get_models_allof_response_200.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/http_validation_error.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/import_.py (98%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/json_like_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/mixed_case_response_200.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_from_all_of.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_name.py (98%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_reference_with_periods.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_additional_properties_inlined.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_additional_properties_refed.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_any_json_properties.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_backslash_in_description.py (96%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_circular_ref_a.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_circular_ref_b.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_date_time_property.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_discriminated_union.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_merged_properties.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_merged_properties_string_to_enum.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_no_properties.py (97%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_primitive_additional_properties.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_property_ref.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_recursive_ref.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_union_property.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_union_property_inlined.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_union_property_inlined_apples.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/model_with_union_property_inlined_bananas.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/none.py (98%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/optional_body_body.py (98%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_bodies_multiple_data_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_bodies_multiple_files_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_bodies_multiple_json_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_form_data_inline_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_naming_property_conflict_with_import_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py (99%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/status_code_patterns_response_2xx.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/status_code_patterns_response_2xx_status.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/status_code_patterns_response_4xx.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/test_inline_objects_body.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/test_inline_objects_response_200.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/my_test_api_client/models/validation_error.py (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client => golden-records/my-test-api-client/my_test_api_client}/py.typed (100%)
rename end_to_end_tests/{test-3-1-golden-record/test_3_1_features_client => golden-records/my-test-api-client/my_test_api_client}/types.py (100%)
rename end_to_end_tests/{golden-record => golden-records/my-test-api-client}/pyproject.toml (100%)
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/.gitignore
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/README.md (100%)
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/pyproject.toml (100%)
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/test_3_1_features_client/__init__.py (100%)
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/test_3_1_features_client/api/const/post_const_path.py (100%)
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/test_3_1_features_client/api/prefix_items/post_prefix_items.py (100%)
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/test_3_1_features_client/models/__init__.py (100%)
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/test_3_1_features_client/models/post_const_path_body.py (100%)
rename end_to_end_tests/{test-3-1-golden-record => golden-records/test-3-1-features-client}/test_3_1_features_client/models/post_prefix_items_body.py (100%)
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed
create mode 100644 end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py
create mode 100644 fuzz/test_codegen.py
create mode 100644 openapi_python_client/schema/ref.py
create mode 100644 openapi_python_client/schema/untrusted_string.py
create mode 100644 openapi_python_client/strings.py
delete mode 100644 openapi_python_client/utils.py
create mode 100644 tests/test_parser/test_properties/test_enum_property.py
create mode 100644 tests/test_schema/test_untrusted_string.py
create mode 100644 tests/test_strings.py
create mode 100644 tests/test_templates/test_endpoint_macros.py
delete mode 100644 tests/test_utils.py
diff --git a/.changeset/arbitrary_code_generation_vulnerability.md b/.changeset/arbitrary_code_generation_vulnerability.md
new file mode 100644
index 000000000..d92098836
--- /dev/null
+++ b/.changeset/arbitrary_code_generation_vulnerability.md
@@ -0,0 +1,13 @@
+---
+default: security
+---
+
+# Arbitrary code generation vulnerability
+
+Prior to this release, malicious OpenAPI documents could cause openapi-python-client to generate arbitrary code, which
+would then be executed by consumers of the generated client.
+
+If you generate code from OpenAPI documents you don't control, you should upgrade to this release as **soon as possible**
+and validate any previously-generated code.
+
+See [the GitHub advisory](https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-5293-mq8x-g3xj) for more details.
diff --git a/.changeset/breaking_changes_for_all_custom_templates.md b/.changeset/breaking_changes_for_all_custom_templates.md
new file mode 100644
index 000000000..232cd3174
--- /dev/null
+++ b/.changeset/breaking_changes_for_all_custom_templates.md
@@ -0,0 +1,22 @@
+---
+default: note
+---
+
+# Breaking changes for all custom templates
+
+**ALL** custom templates are expected to break with this version as a result of the security fix.
+
+1. The `utils` global has been renamed to `strings`
+2. Most string values can no longer be rendered directly into templates,
+ you must describe how the value is being used so it can be properly escaped using either a Python function or Jinja filter:
+ 1. `strings.snake_case()` / `| snakecase` (existing)
+ 2. `strings.kebab_case()` / `| kebabcase` (existing)
+ 3. `strings.pascal_case()` / `| pascalcase` (existing)
+ 4. `python_identifier()` (existing)
+ 5. `class_name()` (existing)
+ 6. `strings.safe_for_docstring()` / `| safe_for_docstring` for values which get injected into a `"""` docstring
+ 7. `strings.in_f_string_literal()` / `| in_f_string_literal` for values that go into `f""` f-strings
+ 8. `strings.in_double_quote_literal()` / `| in_double_quote_literal` for values that go into **non-f-string** `""` literals
+ 9. `.as_unembedded_code()` / `| as_unembedded_code` ONLY for `PythonCode` values—those that are intended to be Python code which is not embedded into any string/docstring. Examples include usages of `.python_code`, `.get_type_string()`, `.get_instance_type_string()`, `.get_type_strings_in_union()`. You *should not* assume these values are safe to put in docstrings, string literals, or f-string literals. Use the dedicated helpers for those.
+As always, you can check the diff of the built in templates for examples. You will also want to check generated output
+for "UntrustedString", which is how any string now requiring one of those functions will appear.
diff --git a/.changeset/fixed_invalid_identifiers_when_resolving_naming_conflicts.md b/.changeset/fixed_invalid_identifiers_when_resolving_naming_conflicts.md
new file mode 100644
index 000000000..7c9d57b3a
--- /dev/null
+++ b/.changeset/fixed_invalid_identifiers_when_resolving_naming_conflicts.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+# Fixed invalid Python identifiers when resolving naming conflicts
+
+When two property or parameter names conflicted after conversion to `snake_case` (e.g. `foo-bar` and `fooBar`), the conflict-resolution path preserved delimiters like `-`, `.`, and spaces in the generated Python identifiers, producing invalid code which failed generation. Conflicting names now keep their original casing but have any characters which are invalid in Python identifiers stripped (e.g. `foobar` and `fooBar`).
diff --git a/.changeset/many_control_characters_now_stripped_from_string_literals.md b/.changeset/many_control_characters_now_stripped_from_string_literals.md
new file mode 100644
index 000000000..9e9ed3f23
--- /dev/null
+++ b/.changeset/many_control_characters_now_stripped_from_string_literals.md
@@ -0,0 +1,11 @@
+---
+default: note
+---
+
+# Many control characters now stripped from string literals
+
+Out of an abundance of caution, most Unicode control characters are now stripped from string literals.
+If your API uses control characters as part of const values, enums, or JSON body property names you may have issues
+with this new version.
+
+Most APIs are not expected to be affected by this change.
diff --git a/.changeset/render_error_data_as_json.md b/.changeset/render_error_data_as_json.md
new file mode 100644
index 000000000..246a70948
--- /dev/null
+++ b/.changeset/render_error_data_as_json.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+# Improve readability of error messages
+
+Errors and warnings which include a snippet of your OpenAPI document now render that snippet as JSON, making them much easier to read.
diff --git a/.changeset/stopped_generating_empty_docstrings_for_models_with_no_description.md b/.changeset/stopped_generating_empty_docstrings_for_models_with_no_description.md
new file mode 100644
index 000000000..505364710
--- /dev/null
+++ b/.changeset/stopped_generating_empty_docstrings_for_models_with_no_description.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+# Stopped generating empty docstrings for models with no description
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
new file mode 100644
index 000000000..02e1be675
--- /dev/null
+++ b/.github/workflows/fuzz.yml
@@ -0,0 +1,33 @@
+name: Fuzz code generation
+
+permissions:
+ contents: read
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: "23 8 * * *"
+ workflow_dispatch:
+
+jobs:
+ fuzz:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ env:
+ OPENAPI_PYTHON_CLIENT_FUZZ_EXAMPLES: ${{ github.event_name == 'schedule' && '1000' || '100' }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Set up Python
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: "3.14"
+ cache: pip
+ - name: Install PDM
+ run: pip install pdm
+ - name: Install dependencies
+ run: pdm sync --clean
+ - name: Fuzz generated Python
+ run: pdm fuzz
diff --git a/.gitignore b/.gitignore
index 1cd5f6801..f7fcfaff6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,5 @@ test-reports/
htmlcov/
# Generated end to end test data
-my-test-api-client/
-custom-e2e/
-3-1-features-client
-tests/tmp
\ No newline at end of file
+tests/tmp
+end_to_end_tests/tmp/*
\ No newline at end of file
diff --git a/end_to_end_tests/__init__.py b/end_to_end_tests/__init__.py
index 3793e0395..f165cc2c5 100644
--- a/end_to_end_tests/__init__.py
+++ b/end_to_end_tests/__init__.py
@@ -1,4 +1,5 @@
-""" Generate a complete client and verify that it is correct """
+"""Generate a complete client and verify that it is correct"""
+
import pytest
pytest.register_assert_rewrite("end_to_end_tests.end_to_end_test_helpers")
diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr
index 6f4a9f2d2..0e6c6eee6 100644
--- a/end_to_end_tests/__snapshots__/test_end_to_end.ambr
+++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr
@@ -34,12 +34,12 @@
Warning(s) encountered while generating. Client was generated, but some pieces may be missing
WARNING parsing PUT / within default. Endpoint will not be generated.
- cannot parse parameter of endpoint put_: Invalid UUID value: 3
+ cannot parse parameter of endpoint put: Invalid UUID value: 3
WARNING parsing POST / within default. Endpoint will not be generated.
- cannot parse parameter of endpoint post_: Invalid UUID value: notauuid
+ cannot parse parameter of endpoint post: Invalid UUID value: notauuid
If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose
@@ -69,7 +69,13 @@
Path parameter must be required
- Parameter(name='optional', param_in=