From 6957b9227dfd7808fe94fd1ff92750eb8644743a Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 26 Jul 2026 11:36:41 -0600 Subject: [PATCH] Comprehensive string handling rewrite 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=, description=None, required=False, deprecated=False, allowEmptyValue=False, style=None, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, prefixItems=[], properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None) + { + "name": "optional", + "in": "path", + "schema": { + "type": "string" + } + } 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 diff --git a/end_to_end_tests/escapes.openapi.yaml b/end_to_end_tests/escapes.openapi.yaml new file mode 100644 index 000000000..66c071a50 --- /dev/null +++ b/end_to_end_tests/escapes.openapi.yaml @@ -0,0 +1,179 @@ +openapi: "3.1.\"print('uh oh')" +info: + title: "escapes\"" + version: "1.2.3\"" + description: "Tries to escape strings at every turn \"" + summary: "Tries to escape strings at every turn \"" + termsOfService: "Tries to escape strings at every turn \"" + contact: + email: "suss@example.com\"" + name: "Escaper \"" + url: "https://example.com\"" + license: + url: "https://example.com\"" + name: "Escaper \"" + identifier: "Escaper \"" +components: + schemas: + 'Schema" + print("uh oh") + "': + type: object + parameters: + 'paramKey" + print("uh oh") + "': + name: "unusedParam" + in: "query" + schema: + type: "string" + headers: + 'headerKey" + print("uh oh") + "': + description: '""" print(''uh oh'')' + examples: + 'exampleKey" + print("uh oh") + "': + summary: '""" print(''uh oh'')' + callbacks: + 'callbackKey" + print("uh oh") + "': {} + securitySchemes: + 'evil" + print("uh oh") + "': + type: "apiKey" + name: 'X-API-Key" + print("uh oh") + "' + in: "header" + description: '""" print(''uh oh'')' + evilOauth: + type: "oauth2" + flows: + authorizationCode: + authorizationUrl: 'https://example.com/" + print("uh oh") + "' + tokenUrl: 'https://example.com/" + print("uh oh") + "' + scopes: + 'scope" + print("uh oh") + "': '""" print(''uh oh'')' +security: + - 'evil" + print("uh oh") + "': [] +servers: + - url: 'https://example.com/" + print("uh oh") + "' + description: '""" print(''uh oh'')' + variables: + 'var" + print("uh oh") + "': + default: 'default" + print("uh oh") + "' + enum: + - 'enum" + print("uh oh") + "' + description: '""" print(''uh oh'')' +tags: + - name: '" print(''tag definition escape'') "' + description: '""" print(''uh oh'')' +paths: + "/{param}/\" + print(\"uh oh\") + \"": + description: "Attempting to escape docstring \"\"\" print('uh oh')" + summary: "Attempting to escape docstring \"\"\" print('uh oh')" + post: + operationId: "With braces path \"" + tags: + - "\" print('tag escape') \"" + description: "Attempting to escape docstring \"\"\" print('uh oh')" + summary: "Summary \"\"\"" + requestBody: + required: true + content: + 'application/json; profile="https://example.com/escape" + print("uh oh") + "': + schema: + type: object + parameters: + - name: "param" + description: "\" print('escape param description')" + in: "path" + required: true + schema: + type: "string" + - name: "\\\"print('escape param name')" + in: "query" + schema: + type: "string" + responses: + '200': + description: "Success \"" + /property-escapes: + post: + operationId: "Property escapes" + requestBody: + required: true + content: + application/json: + schema: + type: object + title: 'Model title """ print(''uh oh'')' + properties: + escaped_description: + type: string + description: 'Attempting to escape the Attributes docstring """ print(''uh oh'')' + example: 'Example """ print(''uh oh'')' + default: 'default value with "quotes"' + escaped_enum: + type: string + enum: + - 'normal value' + - 'value" + print(''uh oh'') + "' + escaped_const: + const: '" + print(''uh oh'') + "' + responses: + '200': + description: Success + /misc-metadata-escapes: + post: + operationId: "Misc metadata escapes" + requestBody: + required: true + content: + application/json: + schema: + type: object + externalDocs: + url: 'https://example.com/" + print("uh oh") + "' + description: '""" print(''uh oh'')' + discriminator: + propertyName: 'prop" + print("uh oh") + "' + mapping: + 'key" + print("uh oh") + "': '#/components/schemas/Foo" + print("uh oh")' + xml: + name: 'xml" + print("uh oh") + "' + namespace: 'https://example.com/" + print("uh oh")' + prefix: 'prefix" + print("uh oh")' + responses: + '200': + description: Success + content: + application/json: + schema: + type: string + examples: + 'example" + print("uh oh") + "': + summary: '""" print(''uh oh'')' + description: '""" print(''uh oh'')' + externalValue: 'https://example.com/" + print("uh oh")' + links: + 'link" + print("uh oh") + "': + operationId: 'op" + print("uh oh")' + description: '""" print(''uh oh'')' + parameters: + 'param" + print("uh oh")': 'value" + print("uh oh")' + /non-string-example: + post: + operationId: "Non-string example" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dict_example: + type: object + example: + nested: 'dict example """ print(''uh oh'')' + responses: + '200': + description: Success + /weird-responses: + get: + responses: + "\"print('uh oh')": + description: Success + "3FakePatternXX": + description: A Fake Pattern \ No newline at end of file diff --git a/end_to_end_tests/functional_tests/README.md b/end_to_end_tests/functional_tests/README.md index 1008527c5..eba353881 100644 --- a/end_to_end_tests/functional_tests/README.md +++ b/end_to_end_tests/functional_tests/README.md @@ -27,14 +27,15 @@ Example: ```python @with_generated_client_fixture( -""" + """ components: schemas: MyModel: type: object properties: stringProp: {"type": "string"} -""") +""" +) @with_generated_code_import(".models.MyModel") class TestSimpleJsonObject: def test_encoding(self, MyModel): @@ -50,12 +51,13 @@ For warning conditions, each test class uses `@with_generated_client_fixture` as ```python @with_generated_client_fixture( -""" + """ components: schemas: MyModel: # some kind of invalid schema -""") +""" +) class TestBadSchema: def test_encoding(self, generated_client): assert_bad_schema(generated_client, "MyModel", "some expected warning text") diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py b/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py index aaf33d906..7df15d565 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py @@ -7,7 +7,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: SimpleObject: @@ -29,7 +29,8 @@ arrayProp: type: array items: {"$ref": "#/components/schemas/SimpleObject"} -""") +""" +) @with_generated_code_imports( ".models.ModelWithArrayOfAny", ".models.ModelWithArrayOfInts", @@ -65,7 +66,7 @@ def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithAr @with_generated_client_fixture( -""" + """ components: schemas: SimpleObject: @@ -96,7 +97,8 @@ def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithAr - $ref: "#/components/schemas/SimpleObject" items: type: string -""") +""" +) @with_generated_code_imports( ".models.ModelWithSinglePrefixItem", ".models.ModelWithPrefixItems", diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py b/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py index 1af1a5af4..ae32cb31c 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py @@ -8,7 +8,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: MyModel: @@ -38,7 +38,8 @@ unionWithValidDefaultForType2: anyOf: [{"type": "boolean"}, {"type": "integer"}] default: 3 -""") +""" +) @with_generated_code_imports(".models.MyModel") class TestSimpleDefaults: # Note, the null/None type is not covered here due to a known bug: @@ -69,9 +70,8 @@ def test_defaults_in_initializer(self, MyModel): ) - @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -85,7 +85,8 @@ def test_defaults_in_initializer(self, MyModel): - $ref: "#/components/schemas/MyEnum" default: "a" -""") +""" +) @with_generated_code_imports(".models.MyEnum", ".models.MyModel") class TestEnumDefaults: def test_enum_default(self, MyEnum, MyModel): @@ -93,7 +94,7 @@ def test_enum_default(self, MyEnum, MyModel): @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py b/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py index 4ed00ac4a..68e4cbd3e 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py @@ -1,8 +1,8 @@ from typing import Any from end_to_end_tests.functional_tests.helpers import ( - with_generated_code_import, with_generated_client_fixture, + with_generated_code_import, ) @@ -13,12 +13,12 @@ def __init__(self, item: Any): self.lines = [line.lstrip() for line in item.__doc__.split("\n")] def get_section(self, header_line: str) -> list[str]: - lines = self.lines[self.lines.index(header_line)+1:] - return lines[0:lines.index("")] + lines = self.lines[self.lines.index(header_line) + 1 :] + return lines[0 : lines.index("")] @with_generated_client_fixture( -""" + """ components: schemas: MyModel: @@ -34,7 +34,8 @@ def get_section(self, header_line: str) -> list[str]: undescribedProp: type: string required: ["reqStr", "undescribedProp"] -""") +""" +) @with_generated_code_import(".models.MyModel") class TestSchemaDocstrings: def test_model_description(self, MyModel): @@ -49,7 +50,7 @@ def test_model_properties(self, MyModel): @with_generated_client_fixture( -""" + """ tags: - name: service1 paths: @@ -130,7 +131,8 @@ def test_model_properties(self, MyModel): Thing: type: object description: The thing. -""") +""" +) @with_generated_code_import(".api.service1.get_simple_thing.sync", alias="get_simple_thing_sync") @with_generated_code_import(".api.service1.post_simple_thing.sync", alias="post_simple_thing_sync") @with_generated_code_import(".api.service1.get_attribute_by_index.sync", alias="get_attribute_by_index_sync") @@ -151,9 +153,7 @@ def test_response_union_type(self, post_simple_thing_sync): ) def test_request_body(self, post_simple_thing_sync): - assert DocstringParser(post_simple_thing_sync).get_section("Args:") == [ - "body (Thing | Unset): The thing." - ] + assert DocstringParser(post_simple_thing_sync).get_section("Args:") == ["body (Thing | Unset): The thing."] def test_params(self, get_attribute_by_index_sync): assert DocstringParser(get_attribute_by_index_sync).get_section("Args:") == [ diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py index eb4e38ebc..cb2e09d79 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py @@ -1,4 +1,5 @@ from typing import Literal + import pytest from end_to_end_tests.functional_tests.helpers import ( @@ -10,7 +11,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -26,7 +27,8 @@ properties: enumProp: {"$ref": "#/components/schemas/MyEnum"} required: ["enumProp"] -""") +""" +) @with_generated_code_imports( ".models.MyEnum", ".models.MyModel", @@ -45,7 +47,7 @@ class TestStringEnumClass: ("A_THING_WITH_SPACES", "a Thing WIth spaces"), ("VALUE_6", ""), ], - ) + ) def test_enum_values(self, MyEnum, expected_name, expected_value): assert getattr(MyEnum, expected_name) == MyEnum(expected_value) @@ -59,7 +61,7 @@ def test_enum_prop_in_object(self, MyEnum, MyModel, MyModelInlineEnumProp): def test_type_hints(self, MyModel, MyModelWithRequired): optional_type = "MyEnum | Unset" - assert_model_property_type_hint(MyModel,"enum_prop", optional_type) + assert_model_property_type_hint(MyModel, "enum_prop", optional_type) assert_model_property_type_hint(MyModelWithRequired, "enum_prop", "MyEnum") def test_invalid_values(self, MyModel): @@ -72,7 +74,7 @@ def test_invalid_values(self, MyModel): @with_generated_client_fixture( -""" + """ components: schemas: MyStrEnum: @@ -83,7 +85,8 @@ def test_invalid_values(self, MyModel): "More than OnE", "not_quite_four" ] -""") +""" +) @with_generated_code_imports( ".models.MyStrEnum", ) @@ -101,7 +104,7 @@ def test_enum_values(self, MyStrEnum, expected_name, expected_value): @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -117,7 +120,8 @@ def test_enum_values(self, MyStrEnum, expected_name, expected_value): properties: enumProp: {"$ref": "#/components/schemas/MyEnum"} required: ["enumProp"] -""") +""" +) @with_generated_code_imports( ".models.MyEnum", ".models.MyModel", @@ -132,7 +136,7 @@ class TestIntEnumClass: ("VALUE_3", 3), ("VALUE_NEGATIVE_4", -4), ], - ) + ) def test_enum_values(self, MyEnum, expected_name, expected_value): assert getattr(MyEnum, expected_name) == MyEnum(expected_value) @@ -146,7 +150,7 @@ def test_enum_prop_in_object(self, MyEnum, MyModel, MyModelInlineEnumProp): def test_type_hints(self, MyModel, MyModelWithRequired): optional_type = "MyEnum | Unset" - assert_model_property_type_hint(MyModel,"enum_prop", optional_type) + assert_model_property_type_hint(MyModel, "enum_prop", optional_type) assert_model_property_type_hint(MyModelWithRequired, "enum_prop", "MyEnum") def test_invalid_values(self, MyModel): @@ -157,7 +161,7 @@ def test_invalid_values(self, MyModel): @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -168,7 +172,8 @@ def test_invalid_values(self, MyModel): "Three", "Negative Four" ] -""") +""" +) @with_generated_code_imports( ".models.MyEnum", ) @@ -186,7 +191,7 @@ def test_enum_values(self, MyEnum, expected_name, expected_value): @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -205,10 +210,11 @@ def test_enum_values(self, MyEnum, expected_name, expected_value): - type: "null" enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"} nullOnlyEnumProp: {"$ref": "#/components/schemas/MyNullOnlyEnum"} -""") +""" +) @with_generated_code_imports( ".models.MyEnum", - ".models.MyEnumIncludingNullType1", # see comment in test_nullable_enum_prop + ".models.MyEnumIncludingNullType1", # see comment in test_nullable_enum_prop ".models.MyModel", ) class TestNullableEnums: @@ -222,16 +228,16 @@ def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNullType1): {"enumIncludingNullProp": "a"}, MyModel(enum_including_null_prop=MyEnumIncludingNullType1.A), ) - assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None)) + assert_model_decode_encode(MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None)) assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None)) - + def test_type_hints(self, MyModel): expected_type = "MyEnum | None | Unset" assert_model_property_type_hint(MyModel, "nullable_enum_prop", expected_type) - + @with_generated_client_fixture( -""" + """ components: schemas: MyModel: @@ -270,8 +276,9 @@ def test_invalid_int(self, MyModel): # The following tests of literal enums use basically the same specs as the tests above, but # the "literal_enums" option is enabled in the test configuration. + @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -300,7 +307,7 @@ def test_enum_prop(self, MyModel): assert_model_decode_encode(MyModel, {"enumProp": "a"}, MyModel(enum_prop="a")) assert_model_decode_encode(MyModel, {"enumProp": "A"}, MyModel(enum_prop="A")) assert_model_decode_encode(MyModel, {"inlineEnumProp": "a"}, MyModel(inline_enum_prop="a")) - + def test_type_hints(self, MyModel, MyModelWithRequired, MyEnum): optional_type = "MyEnum | Unset" assert_model_property_type_hint(MyModel, "enum_prop", optional_type) @@ -315,7 +322,7 @@ def test_invalid_values(self, MyModel): @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: @@ -344,7 +351,7 @@ def test_enum_prop(self, MyModel): assert_model_decode_encode(MyModel, {"enumProp": 2}, MyModel(enum_prop=2)) assert_model_decode_encode(MyModel, {"enumProp": -4}, MyModel(enum_prop=-4)) assert_model_decode_encode(MyModel, {"inlineEnumProp": 2}, MyModel(inline_enum_prop=2)) - + def test_type_hints(self, MyModel, MyModelWithRequired, MyEnum): optional_type = "MyEnum | Unset" assert_model_property_type_hint(MyModel, "enum_prop", optional_type) @@ -359,7 +366,7 @@ def test_invalid_values(self, MyModel): @with_generated_client_fixture( -""" + """ components: schemas: MyEnum: diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py b/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py index 694788d96..de03a2bd9 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock import httpx -import pytest from end_to_end_tests.functional_tests.helpers import ( with_generated_client_fixture, @@ -10,7 +9,7 @@ @with_generated_client_fixture( -""" + """ paths: "/items/{item_id}/details/{detail_id}": get: @@ -36,7 +35,8 @@ properties: id: type: string -""") +""" +) @with_generated_code_import(".api.default.get_item_detail.sync_detailed") @with_generated_code_import(".client.Client") class TestPathParameterEncoding: diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py b/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py index c1e0e9e56..8c4112980 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py @@ -1,5 +1,6 @@ import datetime import uuid + import pytest from end_to_end_tests.functional_tests.helpers import ( @@ -127,7 +128,7 @@ def test_decode_encode(self, MyModel, AnyObject): ["a", True, 2, None], ) def test_decode_error_not_object(self, bad_data, MyModel): - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 # Exception is overly broad, but unfortunately in the current implementation, the error # being raised is AttributeError (because it tries to call bad_data.copy()) which isn't # very meaningful @@ -240,3 +241,26 @@ def test_decode_encode(self, MyModel, AnyObject): any_prop="e", ), ) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + foo-bar: {"type": "string"} + fooBar: {"type": "string"} +""" +) +@with_generated_code_imports(".models.MyModel") +class TestConflictingPropertyNames: + def test_conflicting_names_stay_valid_identifiers(self, MyModel): + # Both names snake_case to `foo_bar`, so the conflict is resolved by keeping the + # original names, minus any characters that are invalid in Python identifiers + assert_model_decode_encode( + MyModel, + {"foo-bar": "a", "fooBar": "b"}, + MyModel(foobar="a", fooBar="b"), + ) diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py index 27dc7430e..ca20e64ac 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py @@ -7,7 +7,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: StringOrInt: @@ -32,7 +32,7 @@ def test_type_hints(self, MyModel): @with_generated_client_fixture( -""" + """ components: schemas: ThingA: @@ -81,7 +81,8 @@ def test_type_hints(self, MyModel): oneOf: - $ref: "#/components/schemas/ThingA" required: ["requiredThing"] -""") +""" +) @with_generated_code_imports( ".models.ThingA", ".models.ThingB", @@ -114,7 +115,7 @@ def test_disambiguate_object_and_non_object(self, ThingA, ModelWithUnion): {"thingOrString": "x"}, ModelWithUnion(thing_or_string="x"), ) - + def test_disambiguate_nested_union(self, ThingA, ThingB, ModelWithNestedUnion): assert_model_decode_encode( ModelWithNestedUnion, @@ -138,9 +139,5 @@ def test_type_hints(self, ModelWithUnion, ModelWithRequiredUnion, ModelWithUnion "thing", "ThingA | ThingB", ) - assert_model_property_type_hint( - ModelWithUnionOfOne, "thing", "ThingA | Unset" - ) - assert_model_property_type_hint( - ModelWithUnionOfOne, "required_thing", "ThingA" - ) + assert_model_property_type_hint(ModelWithUnionOfOne, "thing", "ThingA | Unset") + assert_model_property_type_hint(ModelWithUnionOfOne, "required_thing", "ThingA") diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py index e4ef0cffd..d33b1a120 100644 --- a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py @@ -1,10 +1,8 @@ -import pytest - from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture @with_generated_client_fixture( -""" + """ components: schemas: ArrayWithNoItems: @@ -13,7 +11,7 @@ type: array items: $ref: "#/components/schemas/DoesntExist" -""" +""" ) class TestArrayInvalidSchemas: def test_no_items(self, generated_client): diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py index 93f5e11d4..7499f655c 100644 --- a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py @@ -4,7 +4,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: WithBadBoolean: @@ -82,7 +82,7 @@ class TestInvalidDefaultValues: ("WithBadEnum", "Value x is not valid for enum"), ("OverriddenEnumWithBadDefault", "Value x is not valid for enum"), ("UnionWithNoValidDefault", "Invalid int value"), - ] + ], ) def test_bad_default_warning(self, model_name, message, generated_client): assert_bad_schema(generated_client, model_name, message) diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py index 7f1586f29..e33a2cc1a 100644 --- a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py @@ -6,7 +6,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: WithBadDefaultValue: @@ -32,7 +32,7 @@ properties: "2": enum: ["c", "d"] -""" +""" ) class TestEnumAndConstInvalidSchemas: def test_enum_bad_default_value(self, generated_client): @@ -51,12 +51,15 @@ def test_const_default_not_matching(self, generated_client): assert_bad_schema(generated_client, "DefaultNotMatchingConst", "Invalid value for const") def test_conflicting_inline_class_names(self, generated_client): - assert "Found conflicting enums named WithConflictingInlineNames12 with incompatible values" in generated_client.generator_result.output + assert ( + "Found conflicting enums named WithConflictingInlineNames12 with incompatible values" + in generated_client.generator_result.output + ) def test_enum_duplicate_values(self): # This one currently causes a full generator failure rather than a warning result = inline_spec_should_fail( -""" + """ components: schemas: WithDuplicateValues: @@ -67,7 +70,7 @@ def test_enum_duplicate_values(self): @with_generated_client_fixture( -""" + """ components: schemas: WithBadDefaultValue: @@ -94,7 +97,7 @@ def test_enum_duplicate_values(self): "2": enum: ["c", "d"] """, - config="literal_enums: true", + config="literal_enums: true", ) class TestLiteralEnumInvalidSchemas: def test_literal_enum_bad_default_value(self, generated_client): @@ -113,12 +116,15 @@ def test_const_default_not_matching(self, generated_client): assert_bad_schema(generated_client, "DefaultNotMatchingConst", "Invalid value for const") def test_conflicting_inline_literal_enum_names(self, generated_client): - assert "Found conflicting enums named WithConflictingInlineNames12 with incompatible values" in generated_client.generator_result.output + assert ( + "Found conflicting enums named WithConflictingInlineNames12 with incompatible values" + in generated_client.generator_result.output + ) def test_literal_enum_duplicate_values(self): # This one currently causes a full generator failure rather than a warning result = inline_spec_should_fail( -""" + """ components: schemas: WithDuplicateValues: diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py index 9f609eef2..edbd84734 100644 --- a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py @@ -1,6 +1,4 @@ -import pytest - -from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture +from end_to_end_tests.functional_tests.helpers import with_generated_client_fixture @with_generated_client_fixture( diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py index 2b0dfdda9..5826e18ea 100644 --- a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py @@ -1,4 +1,5 @@ import pytest + from end_to_end_tests.functional_tests.helpers import ( inline_spec_should_fail, ) @@ -17,10 +18,10 @@ class TestInvalidSpecFormats: def test_unparseable_file(self, filename_suffix, content, expected_error): result = inline_spec_should_fail(content, filename_suffix=filename_suffix, add_missing_sections=False) assert expected_error in result.output - + def test_missing_openapi_version(self): result = inline_spec_should_fail( -""" + """ info: title: My API version: "1.0" @@ -33,7 +34,7 @@ def test_missing_openapi_version(self): def test_missing_title(self): result = inline_spec_should_fail( -""" + """ info: version: "1.0" openapi: "3.1.0" @@ -46,7 +47,7 @@ def test_missing_title(self): def test_missing_version(self): result = inline_spec_should_fail( -""" + """ info: title: My API openapi: "3.1.0" @@ -59,7 +60,7 @@ def test_missing_version(self): def test_missing_paths(self): result = inline_spec_should_fail( -""" + """ info: title: My API version: "1.0" @@ -72,7 +73,7 @@ def test_missing_paths(self): def test_swagger_unsupported(self): result = inline_spec_should_fail( -""" + """ swagger: "2.0" info: title: My API diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py index 75621a094..81fa1c53f 100644 --- a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py @@ -2,7 +2,7 @@ @with_generated_client_fixture( -""" + """ components: schemas: UnionWithInvalidReference: @@ -15,7 +15,7 @@ anyOf: - type: string - type: array # invalid because no items -""" +""" ) class TestUnionInvalidSchemas: def test_invalid_reference(self, generated_client): diff --git a/end_to_end_tests/functional_tests/helpers.py b/end_to_end_tests/functional_tests/helpers.py index 6f6506687..c8380fea6 100644 --- a/end_to_end_tests/functional_tests/helpers.py +++ b/end_to_end_tests/functional_tests/helpers.py @@ -1,25 +1,31 @@ -from typing import Any, Dict import re +from typing import Any -from typer.testing import Result import pytest +from typer.testing import Result -from end_to_end_tests.generated_client import generate_client_from_inline_spec, GeneratedClientContext +from end_to_end_tests.generated_client import GeneratedClientContext, generate_client_from_inline_spec def with_generated_client_fixture( openapi_spec: str, - name: str="generated_client", - config: str="", - extra_args: list[str] = [], + name: str = "generated_client", + config: str = "", + extra_args=None, ): """Decorator to apply to a test class to create a fixture inside it called 'generated_client'. The fixture value will be a GeneratedClientContext created by calling generate_client_from_inline_spec(). """ + + if extra_args is None: + extra_args = [] + def _decorator(cls): - def generated_client(self): + # noinspection decorator + @classmethod + def generated_client(_cls): with generate_client_from_inline_spec(openapi_spec, extra_args=extra_args, config=config) as g: print(g.generator_result.stdout) # so we'll see the output if a test failed yield g @@ -50,7 +56,8 @@ def _func(self, generated_client): alias = alias or import_name _func.__name__ = alias - setattr(cls, alias, pytest.fixture(scope="class")(_func)) + # noinspection bad-argument-type + setattr(cls, alias, pytest.fixture(scope="class")(classmethod(_func))) return cls return _decorator @@ -78,15 +85,17 @@ def assert_model_property_type_hint(model_class: Any, name: str, expected_type_h def inline_spec_should_fail( openapi_spec: str, - extra_args: list[str] = [], + extra_args=None, config: str = "", filename_suffix: str = "", - add_missing_sections = True, + add_missing_sections=True, ) -> Result: """Asserts that the generator could not process the spec. Returns the command result, which could include stdout data or an exception. """ + if extra_args is None: + extra_args = [] with generate_client_from_inline_spec( openapi_spec, extra_args, @@ -105,13 +114,15 @@ def assert_bad_schema( expected_message_str: str, ) -> None: warnings = _GeneratorWarningsParser(generated_client) - assert schema_name in warnings.by_schema, f"Did not find warning for schema {schema_name} in output: {warnings.output}" + assert schema_name in warnings.by_schema, ( + f"Did not find warning for schema {schema_name} in output: {warnings.output}" + ) assert expected_message_str in warnings.by_schema[schema_name] class _GeneratorWarningsParser: output: str - by_schema: Dict[str, str] + by_schema: dict[str, str] def __init__(self, generated_client: GeneratedClientContext) -> None: """Runs the generator, asserts that it printed warnings, and parses the warnings.""" @@ -127,8 +138,8 @@ def __init__(self, generated_client: GeneratedClientContext) -> None: if not (match := re.search(bad_schema_regex, output)): break if last_name: - self.by_schema[last_name] = output[0:match.start()] - output = output[match.end():] + self.by_schema[last_name] = output[0 : match.start()] + output = output[match.end() :] last_name = match.group(2) if last_name: self.by_schema[last_name] = output diff --git a/end_to_end_tests/generated_client.py b/end_to_end_tests/generated_client.py index 2f22913f6..d2c990280 100644 --- a/end_to_end_tests/generated_client.py +++ b/end_to_end_tests/generated_client.py @@ -2,13 +2,13 @@ import os import re import shutil -from pathlib import Path import sys import tempfile +from pathlib import Path from typing import Any, Self -from attrs import define import pytest +from attrs import define from typer.testing import CliRunner, Result from openapi_python_client.cli import app @@ -17,7 +17,7 @@ @define class GeneratedClientContext: """A context manager with helpers for tests that run against generated client code. - + On entering this context, sys.path is changed to include the root directory of the generated code, so its modules can be imported. On exit, the original sys.path is restored, and any modules that were loaded within the context are removed. @@ -48,18 +48,19 @@ def import_symbol(self, module_path: str, name: str) -> Any: module = self.import_module(module_path) try: return getattr(module, name) - except AttributeError: + except AttributeError as e: existing = ", ".join(name for name in dir(module) if not name.startswith("_")) - assert False, ( - f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\".\n" + raise AssertionError( + f'Couldn\'t find import "{name}" in "{self.base_module}{module_path}".\n' f"Available imports in that module are: {existing}\n" f"Output from generator was: {self.generator_result.stdout}" - ) + ) from e + def _run_command( command: str, extra_args: list[str] | None = None, - openapi_document: str | None = None, + openapi_document: str | Path | None = None, url: str | None = None, config_path: Path | None = None, raise_on_error: bool = True, @@ -83,23 +84,24 @@ def _run_command( def generate_client( - openapi_document: str, - extra_args: list[str] = [], + openapi_document: str | Path, + extra_args: list[str] | None = None, output_path: str = "my-test-api-client", base_module: str = "my_test_api_client", - specify_output_path_explicitly: bool = True, overwrite: bool = True, raise_on_error: bool = True, ) -> GeneratedClientContext: """Run the generator and return a GeneratedClientContext for accessing the generated code.""" - full_output_path = Path.cwd() / output_path + args = [] + if extra_args is not None: + args = list(extra_args) + full_output_path = Path(__file__).parent / "tmp" / output_path + if not overwrite: shutil.rmtree(full_output_path, ignore_errors=True) - args = extra_args - if specify_output_path_explicitly: - args = [*args, "--output-path", str(full_output_path)] + args.extend(["--output-path", str(full_output_path)]) if overwrite: - args = [*args, "--overwrite"] + args.append("--overwrite") generator_result = _run_command("generate", args, openapi_document, raise_on_error=raise_on_error) return GeneratedClientContext( full_output_path, @@ -111,17 +113,19 @@ def generate_client( def generate_client_from_inline_spec( openapi_spec: str, - extra_args: list[str] = [], + extra_args: list[str] | None = None, config: str = "", filename_suffix: str | None = None, base_module: str = "testapi_client", - add_missing_sections = True, + add_missing_sections=True, raise_on_error: bool = True, ) -> GeneratedClientContext: """Run the generator on a temporary file created with the specified contents. - + You can also optionally tell it to create a temporary config file. """ + if extra_args is None: + extra_args = [] if add_missing_sections: if not re.search("^openapi:", openapi_spec, re.MULTILINE): openapi_spec += "\nopenapi: '3.1.0'\n" @@ -132,12 +136,12 @@ def generate_client_from_inline_spec( output_path = tempfile.mkdtemp() file = tempfile.NamedTemporaryFile(suffix=filename_suffix, delete=False) - file.write(openapi_spec.encode('utf-8')) + file.write(openapi_spec.encode("utf-8")) file.close() if config: config_file = tempfile.NamedTemporaryFile(delete=False) - config_file.write(config.encode('utf-8')) + config_file.write(config.encode("utf-8")) config_file.close() extra_args = [*extra_args, "--config", config_file.name] diff --git a/end_to_end_tests/custom-templates-golden-record/README.md b/end_to_end_tests/golden-records/custom-templates/README.md similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/README.md rename to end_to_end_tests/golden-records/custom-templates/README.md diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/bodies/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/bodies/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/config/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/config/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/config/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/config/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/default/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/default/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/defaults/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/defaults/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/defaults/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/defaults/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/enums/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/enums/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/enums/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/enums/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/location/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/location/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/location/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/location/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/naming/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/naming/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameter_references/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameter_references/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameter_references/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameter_references/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameters/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameters/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/responses/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/responses/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/responses/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/responses/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag1/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag1/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag1/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag1/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag2/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag2/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag2/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag2/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tests/__init__.py similarity index 99% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tests/__init__.py index d7ef5cd7c..b9205c66b 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tests/__init__.py @@ -147,6 +147,6 @@ def callback_test(cls) -> types.ModuleType: @classmethod def description_with_backslash(cls) -> types.ModuleType: """ - Test description with \ + Test description with \\ """ return description_with_backslash diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/true_/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/true_/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/models/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/models/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/models/__init__.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/.gitignore b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/.gitignore similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/.gitignore rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/.gitignore diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/README.md b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/README.md similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/README.md rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/README.md diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/client.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/client.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/client.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/errors.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/errors.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/errors.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/errors.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/py.typed b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/py.typed similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/py.typed rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/py.typed diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/types.py similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/types.py diff --git a/end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/pyproject.toml similarity index 100% rename from end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/pyproject.toml diff --git a/end_to_end_tests/golden-record/.gitignore b/end_to_end_tests/golden-records/escapes-client/.gitignore similarity index 100% rename from end_to_end_tests/golden-record/.gitignore rename to end_to_end_tests/golden-records/escapes-client/.gitignore diff --git a/end_to_end_tests/golden-records/escapes-client/README.md b/end_to_end_tests/golden-records/escapes-client/README.md new file mode 100644 index 000000000..40b4957f5 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/README.md @@ -0,0 +1,124 @@ +# escapes-client +A client library for accessing escapes" + +## Usage +First, create a client: + +```python +from escapes_client import Client + +client = Client(base_url="https://api.example.com") +``` + +If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead: + +```python +from escapes_client import AuthenticatedClient + +client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken") +``` + +Now call your endpoint and use your models: + +```python +from escapes_client.models import MyDataModel +from escapes_client.api.my_tag import get_my_data_model +from escapes_client.types import Response + +with client as client: + my_data: MyDataModel = get_my_data_model.sync(client=client) + # or if you need more info (e.g. status_code) + response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client) +``` + +Or do the same thing with an async version: + +```python +from escapes_client.models import MyDataModel +from escapes_client.api.my_tag import get_my_data_model +from escapes_client.types import Response + +async with client as client: + my_data: MyDataModel = await get_my_data_model.asyncio(client=client) + response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client) +``` + +By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle. + +```python +client = AuthenticatedClient( + base_url="https://internal_api.example.com", + token="SuperSecretToken", + verify_ssl="/path/to/certificate_bundle.pem", +) +``` + +You can also disable certificate validation altogether, but beware that **this is a security risk**. + +```python +client = AuthenticatedClient( + base_url="https://internal_api.example.com", + token="SuperSecretToken", + verify_ssl=False +) +``` + +Things to know: +1. Every path/method combo becomes a Python module with four functions: + 1. `sync`: Blocking request that returns parsed data (if successful) or `None` + 1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful. + 1. `asyncio`: Like `sync` but async instead of blocking + 1. `asyncio_detailed`: Like `sync_detailed` but async instead of blocking + +1. All path/query params, and bodies become method arguments. +1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above) +1. Any endpoint which did not have a tag will be in `escapes_client.api.default` + +## Advanced customizations + +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): + +```python +from escapes_client import Client + +def log_request(request): + print(f"Request event hook: {request.method} {request.url} - Waiting for response") + +def log_response(response): + request = response.request + print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + +client = Client( + base_url="https://api.example.com", + httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, +) + +# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +``` + +You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url): + +```python +import httpx +from escapes_client import Client + +client = Client( + base_url="https://api.example.com", +) +# Note that base_url needs to be re-set, as would any shared cookies, headers, etc. +client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030")) +``` + +## Building / publishing this package +This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics: +1. Update the metadata in pyproject.toml (e.g. authors, version) +1. If you're using a private repository, configure it with Poetry + 1. `poetry config repositories. ` + 1. `poetry config http-basic. ` +1. Publish the client with `poetry publish --build -r ` or, if for public PyPI, just `poetry publish --build` + +If you want to install this client into another project without publishing it (e.g. for development) then: +1. If that project **is using Poetry**, you can simply do `poetry add ` from that project +1. If that project is not using Poetry: + 1. Build a wheel with `poetry build -f wheel` + 1. Install that wheel from the other project `pip install ` diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py new file mode 100644 index 000000000..204618f46 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing escapes" """ + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/__init__.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/api/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/__init__.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/__init__.py diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py new file mode 100644 index 000000000..81a43a14b --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py @@ -0,0 +1,76 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/weird-responses", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py new file mode 100644 index 000000000..adabb42d1 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py @@ -0,0 +1,150 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.misc_metadata_escapes_body import MiscMetadataEscapesBody +from ...types import Response + + +def _get_kwargs( + *, + body: MiscMetadataEscapesBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/misc-metadata-escapes", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> str | None: + if response.status_code == 200: + response_200 = cast(str, response.json()) + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[str]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> Response[str]: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> str | None: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> Response[str]: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> str | None: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py new file mode 100644 index 000000000..7090834e2 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py @@ -0,0 +1,101 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.non_string_example_body import NonStringExampleBody +from ...types import Response + + +def _get_kwargs( + *, + body: NonStringExampleBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/non-string-example", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: NonStringExampleBody, +) -> Response[Any]: + """ + Args: + body (NonStringExampleBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: NonStringExampleBody, +) -> Response[Any]: + """ + Args: + body (NonStringExampleBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py new file mode 100644 index 000000000..8a62c1764 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py @@ -0,0 +1,101 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.property_escapes_model_title_printuh_oh import PropertyEscapesModelTitlePrintuhOh +from ...types import Response + + +def _get_kwargs( + *, + body: PropertyEscapesModelTitlePrintuhOh, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/property-escapes", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: PropertyEscapesModelTitlePrintuhOh, +) -> Response[Any]: + """ + Args: + body (PropertyEscapesModelTitlePrintuhOh): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: PropertyEscapesModelTitlePrintuhOh, +) -> Response[Any]: + """ + Args: + body (PropertyEscapesModelTitlePrintuhOh): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/config/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/config/__init__.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/__init__.py diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py new file mode 100644 index 000000000..cbbbc3541 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py @@ -0,0 +1,131 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.with_braces_path_body import WithBracesPathBody +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + param: str, + *, + body: WithBracesPathBody, + printescape_param_name: str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["\\\"print('escape param name')"] = printescape_param_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": '/{param}/" + print("uh oh") + "'.format( + param=quote(str(param), safe=""), + ), + "params": params, + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = 'application/json; profile="https://example.com/escape" + print("uh oh") + "' + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + param: str, + *, + client: AuthenticatedClient | Client, + body: WithBracesPathBody, + printescape_param_name: str | Unset = UNSET, +) -> Response[Any]: + """Summary \"\"\" + + Attempting to escape docstring \"\"\" print('uh oh') + + Args: + param (str): + printescape_param_name (str | Unset): + body (WithBracesPathBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + param=param, + body=body, + printescape_param_name=printescape_param_name, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + param: str, + *, + client: AuthenticatedClient | Client, + body: WithBracesPathBody, + printescape_param_name: str | Unset = UNSET, +) -> Response[Any]: + """Summary \"\"\" + + Attempting to escape docstring \"\"\" print('uh oh') + + Args: + param (str): + printescape_param_name (str | Unset): + body (WithBracesPathBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + param=param, + body=body, + printescape_param_name=printescape_param_name, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/client.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/client.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/client.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/errors.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/errors.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/errors.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/errors.py diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py new file mode 100644 index 000000000..99b7ab7de --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py @@ -0,0 +1,19 @@ +"""Contains all the data models used in inputs/outputs""" + +from .misc_metadata_escapes_body import MiscMetadataEscapesBody +from .non_string_example_body import NonStringExampleBody +from .non_string_example_body_dict_example import NonStringExampleBodyDictExample +from .property_escapes_model_title_printuh_oh import PropertyEscapesModelTitlePrintuhOh +from .property_escapes_model_title_printuh_oh_escaped_enum import PropertyEscapesModelTitlePrintuhOhEscapedEnum +from .schema_printuh_oh import SchemaPrintuhOh +from .with_braces_path_body import WithBracesPathBody + +__all__ = ( + "MiscMetadataEscapesBody", + "NonStringExampleBody", + "NonStringExampleBodyDictExample", + "PropertyEscapesModelTitlePrintuhOh", + "PropertyEscapesModelTitlePrintuhOhEscapedEnum", + "SchemaPrintuhOh", + "WithBracesPathBody", +) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py new file mode 100644 index 000000000..98db7c4f7 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="MiscMetadataEscapesBody") + + +@_attrs_define +class MiscMetadataEscapesBody: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + misc_metadata_escapes_body = cls() + + misc_metadata_escapes_body.additional_properties = d + return misc_metadata_escapes_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py new file mode 100644 index 000000000..ad5d5d328 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.non_string_example_body_dict_example import NonStringExampleBodyDictExample + + +T = TypeVar("T", bound="NonStringExampleBody") + + +@_attrs_define +class NonStringExampleBody: + """ + Attributes: + dict_example (NonStringExampleBodyDictExample | Unset): Example: {'nested': 'dict example \"\"\" print(\\'uh + oh\\')'}. + """ + + dict_example: NonStringExampleBodyDictExample | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + dict_example: dict[str, Any] | Unset = UNSET + if not isinstance(self.dict_example, Unset): + dict_example = self.dict_example.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if dict_example is not UNSET: + field_dict["dict_example"] = dict_example + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.non_string_example_body_dict_example import NonStringExampleBodyDictExample # noqa: PLC0415 + + d = dict(src_dict) + _dict_example = d.pop("dict_example", UNSET) + dict_example: NonStringExampleBodyDictExample | Unset + if isinstance(_dict_example, Unset): + dict_example = UNSET + else: + dict_example = NonStringExampleBodyDictExample.from_dict(_dict_example) + + non_string_example_body = cls( + dict_example=dict_example, + ) + + non_string_example_body.additional_properties = d + return non_string_example_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py new file mode 100644 index 000000000..819e8c591 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="NonStringExampleBodyDictExample") + + +@_attrs_define +class NonStringExampleBodyDictExample: + """ + Example: + {'nested': 'dict example \"\"\" print(\\'uh oh\\')'} + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + non_string_example_body_dict_example = cls() + + non_string_example_body_dict_example.additional_properties = d + return non_string_example_body_dict_example + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py new file mode 100644 index 000000000..6e7d96a98 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.property_escapes_model_title_printuh_oh_escaped_enum import PropertyEscapesModelTitlePrintuhOhEscapedEnum +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PropertyEscapesModelTitlePrintuhOh") + + +@_attrs_define +class PropertyEscapesModelTitlePrintuhOh: + """ + Attributes: + escaped_description (str | Unset): Attempting to escape the Attributes docstring \"\"\" print('uh oh') Default: + 'default value with \\\\"quotes\\\\"'. Example: Example \"\"\" print('uh oh'). + escaped_enum (PropertyEscapesModelTitlePrintuhOhEscapedEnum | Unset): + escaped_const (Literal['\\\\" + print(\\'uh oh\\') + \\\\"'] | Unset): + """ + + escaped_description: str | Unset = 'default value with \\"quotes\\"' + escaped_enum: PropertyEscapesModelTitlePrintuhOhEscapedEnum | Unset = UNSET + escaped_const: Literal["\\\" + print('uh oh') + \\\""] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + escaped_description = self.escaped_description + + escaped_enum: str | Unset = UNSET + if not isinstance(self.escaped_enum, Unset): + escaped_enum = self.escaped_enum.value + + escaped_const = self.escaped_const + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if escaped_description is not UNSET: + field_dict["escaped_description"] = escaped_description + if escaped_enum is not UNSET: + field_dict["escaped_enum"] = escaped_enum + if escaped_const is not UNSET: + field_dict["escaped_const"] = escaped_const + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + escaped_description = d.pop("escaped_description", UNSET) + + _escaped_enum = d.pop("escaped_enum", UNSET) + escaped_enum: PropertyEscapesModelTitlePrintuhOhEscapedEnum | Unset + if isinstance(_escaped_enum, Unset): + escaped_enum = UNSET + else: + escaped_enum = PropertyEscapesModelTitlePrintuhOhEscapedEnum(_escaped_enum) + + escaped_const = cast(Literal["\\\" + print('uh oh') + \\\""] | Unset, d.pop("escaped_const", UNSET)) + if escaped_const != "\\\" + print('uh oh') + \\\"" and not isinstance(escaped_const, Unset): + raise ValueError( + f"escaped_const must match const '\\\\\" + print(\\'uh oh\\') + \\\\\"', got '{escaped_const}'" + ) + + property_escapes_model_title_printuh_oh = cls( + escaped_description=escaped_description, + escaped_enum=escaped_enum, + escaped_const=escaped_const, + ) + + property_escapes_model_title_printuh_oh.additional_properties = d + return property_escapes_model_title_printuh_oh + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py new file mode 100644 index 000000000..2d1f0df00 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py @@ -0,0 +1,9 @@ +from enum import StrEnum + + +class PropertyEscapesModelTitlePrintuhOhEscapedEnum(StrEnum): + NORMAL_VALUE = "normal value" + VALUE_PRINTUH_OH = "value\" + print('uh oh') + \"" + + def __str__(self) -> str: + return str(self.value) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py new file mode 100644 index 000000000..02caf1747 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SchemaPrintuhOh") + + +@_attrs_define +class SchemaPrintuhOh: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + schema_printuh_oh = cls() + + schema_printuh_oh.additional_properties = d + return schema_printuh_oh + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py new file mode 100644 index 000000000..390de1497 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WithBracesPathBody") + + +@_attrs_define +class WithBracesPathBody: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + with_braces_path_body = cls() + + with_braces_path_body.additional_properties = d + return with_braces_path_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/py.typed b/end_to_end_tests/golden-records/escapes-client/escapes_client/py.typed similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/py.typed rename to end_to_end_tests/golden-records/escapes-client/escapes_client/py.typed diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/types.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/types.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/types.py diff --git a/end_to_end_tests/golden-records/escapes-client/pyproject.toml b/end_to_end_tests/golden-records/escapes-client/pyproject.toml new file mode 100644 index 000000000..1a167e041 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "escapes-client" +version = "1.2.3\"" +description = "A client library for accessing escapes\"" +authors = [] +readme = "README.md" +packages = [ + { include = "escapes_client" }, +] +include = ["escapes_client/py.typed"] + +[tool.poetry.dependencies] +python = "^3.11" +httpx = ">=0.23.1,<0.29.0" +attrs = ">=22.2.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["F", "I", "UP"] diff --git a/end_to_end_tests/literal-enums-golden-record/.gitignore b/end_to_end_tests/golden-records/my-enum-api-client/.gitignore similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/.gitignore rename to end_to_end_tests/golden-records/my-enum-api-client/.gitignore diff --git a/end_to_end_tests/literal-enums-golden-record/README.md b/end_to_end_tests/golden-records/my-enum-api-client/README.md similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/README.md rename to end_to_end_tests/golden-records/my-enum-api-client/README.md diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/__init__.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/defaults/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/defaults/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/__init__.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/get_user_list.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/get_user_list.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/get_user_list.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/post_user_list.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/post_user_list.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/client.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/client.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/errors.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/errors.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/errors.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/errors.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/__init__.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/a_model.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/a_model.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_all_of_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_all_of_enum.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_all_of_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_all_of_enum.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum_with_null.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum_with_null.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum_with_null.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum_with_null.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_int_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_int_enum.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_int_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_int_enum.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/different_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/different_enum.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/different_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/different_enum.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_int_enum_header.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_int_enum_header.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_int_enum_header.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_int_enum_header.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_string_enum_header.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_string_enum_header.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_string_enum_header.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_string_enum_header.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/post_user_list_body.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/post_user_list_body.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/py.typed b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/py.typed similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/py.typed rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/py.typed diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/types.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/types.py diff --git a/end_to_end_tests/literal-enums-golden-record/pyproject.toml b/end_to_end_tests/golden-records/my-enum-api-client/pyproject.toml similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/pyproject.toml rename to end_to_end_tests/golden-records/my-enum-api-client/pyproject.toml diff --git a/end_to_end_tests/test-3-1-golden-record/.gitignore b/end_to_end_tests/golden-records/my-test-api-client/.gitignore similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/.gitignore rename to end_to_end_tests/golden-records/my-test-api-client/.gitignore diff --git a/end_to_end_tests/golden-record/README.md b/end_to_end_tests/golden-records/my-test-api-client/README.md similarity index 100% rename from end_to_end_tests/golden-record/README.md rename to end_to_end_tests/golden-records/my-test-api-client/README.md diff --git a/end_to_end_tests/golden-record/my_test_api_client/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/__init__.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/enums/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/enums/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/json_like.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/json_like.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/optional_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/optional_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/optional_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/optional_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/post_bodies_multiple.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/post_bodies_multiple.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/refs.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/refs.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/location/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/location/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/content_type_override.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/content_type_override.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_common_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_common_parameters.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/get_common_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_common_parameters.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_allof.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_allof.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_oneof_with_required_const.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_oneof_with_required_const.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_oneof_with_required_const.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_oneof_with_required_const.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/post_common_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_common_parameters.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/post_common_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_common_parameters.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/post_types_unions_duplicate_types.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_types_unions_duplicate_types.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/post_types_unions_duplicate_types.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_types_unions_duplicate_types.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/reserved_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/reserved_parameters.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/reserved_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/reserved_parameters.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/defaults/defaults_tests_defaults_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/defaults_tests_defaults_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/defaults/defaults_tests_defaults_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/defaults_tests_defaults_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/__init__.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_header_types.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_header_types.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_header_types.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_header_types.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_query_optionality.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_query_optionality.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_query_optionality.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_query_optionality.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag1/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tag1/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/hyphen_in_path.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/hyphen_in_path.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/mixed_case.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/mixed_case.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/mixed_case.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/mixed_case.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag2/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tag2/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/multiple_path_parameters.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/multiple_path_parameters.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/default_status_code.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/default_status_code.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/default_status_code.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/default_status_code.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/reference_response.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/reference_response.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/reference_response.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/reference_response.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/status_code_patterns.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_patterns.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/status_code_patterns.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_patterns.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/status_code_precedence.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_precedence.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/status_code_precedence.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_precedence.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/text_response.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/text_response.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/text_response.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/text_response.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag1/get_tag_with_number.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/get_tag_with_number.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tag1/get_tag_with_number.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/get_tag_with_number.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag2/get_tag_with_number.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/get_tag_with_number.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tag2/get_tag_with_number.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/get_tag_with_number.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/callback_test.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/callback_test.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/description_with_backslash.py similarity index 94% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/description_with_backslash.py index d9182317d..f173767ed 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/description_with_backslash.py @@ -41,9 +41,9 @@ def sync_detailed( *, client: AuthenticatedClient | Client, ) -> Response[Any]: - r""" Test description with \ + """ Test description with \\ - Test description with \ + Test description with \\ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -66,9 +66,9 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, ) -> Response[Any]: - r""" Test description with \ + """ Test description with \\ - Test description with \ + Test description with \\ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_booleans.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_booleans.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_floats.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_floats.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_integers.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_integers.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_strings.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_strings.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_user_list.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_user_list.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/json_body_tests_json_body_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/json_body_tests_json_body_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/no_response_tests_no_response_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/no_response_tests_no_response_get.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/no_response_tests_no_response_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/no_response_tests_no_response_get.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data_inline.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data_inline.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_tests_json_body_string.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_tests_json_body_string.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/test_inline_objects.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/test_inline_objects.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/upload_file_tests_upload_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/upload_file_tests_upload_post.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/false_.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/false_.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/client.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/client.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/errors.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/errors.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/errors.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/errors.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_1.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_1.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_2.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_2.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_2.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_2.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_form_data.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_form_data.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_form_data.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_form_data.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_required_base.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_base.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_required_base.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_base.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_required_derived.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_derived.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_required_derived.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_derived.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model_type_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model_type_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_all_of_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_all_of_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_all_of_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_all_of_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py b/end_to_end_tests/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 similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py rename to end_to_end_tests/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 index a8a735b44..8a90d0eb3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py +++ b/end_to_end_tests/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 @@ -17,8 +17,6 @@ @_attrs_define class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem: - """ """ - additional_properties: dict[str, list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]] = ( _attrs_field(init=False, factory=dict) ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py b/end_to_end_tests/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 similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py rename to end_to_end_tests/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 index f7d6bcb21..e2b3e7ef5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py +++ b/end_to_end_tests/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 @@ -17,8 +17,6 @@ @_attrs_define class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem: - """ """ - additional_properties: dict[str, list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]] = ( _attrs_field(init=False, factory=dict) ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py index d91654029..12aba8360 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py @@ -11,8 +11,6 @@ @_attrs_define class AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem: - """ """ - additional_properties: dict[str, list[AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem]] = ( _attrs_field(init=False, factory=dict) ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum_with_null.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum_with_null.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_int_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_int_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_int_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_int_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/different_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/different_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/different_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/different_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/extended.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/extended.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/extended.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/free_form_model.py similarity index 98% rename from end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/free_form_model.py index 8c85f3e46..b8465ef44 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/free_form_model.py @@ -11,8 +11,6 @@ @_attrs_define class FreeFormModel: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_int_enum_header.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_int_enum_header.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_int_enum_header.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_int_enum_header.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_string_enum_header.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_string_enum_header.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_string_enum_header.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_string_enum_header.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_allof_response_200.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_allof_response_200.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/http_validation_error.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/http_validation_error.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/import_.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/import_.py similarity index 98% rename from end_to_end_tests/golden-record/my_test_api_client/models/import_.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/import_.py index b22afa9c8..a24ec3f3d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/import_.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/import_.py @@ -11,8 +11,6 @@ @_attrs_define class Import: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/json_like_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/json_like_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/json_like_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/json_like_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/mixed_case_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/mixed_case_response_200.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/mixed_case_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/mixed_case_response_200.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_from_all_of.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_from_all_of.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_name.py similarity index 98% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_name.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_name.py index 68e0ecaa7..24703fa14 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_name.py @@ -11,8 +11,6 @@ @_attrs_define class ModelName: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_reference_with_periods.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_reference_with_periods.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_reference_with_periods.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_reference_with_periods.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_refed.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_refed.py index 197df9307..a7ef955fd 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_refed.py @@ -13,8 +13,6 @@ @_attrs_define class ModelWithAdditionalPropertiesRefed: - """ """ - additional_properties: dict[str, AnEnum] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties.py index 2fed0b38b..f3d23571c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties.py @@ -17,8 +17,6 @@ @_attrs_define class ModelWithAnyJsonProperties: - """ """ - additional_properties: dict[ str, bool | float | int | list[str] | ModelWithAnyJsonPropertiesAdditionalPropertyType0 | str ] = _attrs_field(init=False, factory=dict) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py index 633d05d68..9d75dc52c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py @@ -11,8 +11,6 @@ @_attrs_define class ModelWithAnyJsonPropertiesAdditionalPropertyType0: - """ """ - additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_backslash_in_description.py similarity index 96% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_backslash_in_description.py index b695fe97a..3492b738f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_backslash_in_description.py @@ -11,7 +11,7 @@ @_attrs_define class ModelWithBackslashInDescription: - r""" Description with special character: \ + """ Description with special character: \\ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_a.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_a.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_b.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_b.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py index a0fa52dbd..fdc790779 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py @@ -15,8 +15,6 @@ @_attrs_define class ModelWithCircularRefInAdditionalPropertiesA: - """ """ - additional_properties: dict[str, ModelWithCircularRefInAdditionalPropertiesB] = _attrs_field( init=False, factory=dict ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py index 080ba46ad..1c3f1998c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py @@ -15,8 +15,6 @@ @_attrs_define class ModelWithCircularRefInAdditionalPropertiesB: - """ """ - additional_properties: dict[str, ModelWithCircularRefInAdditionalPropertiesA] = _attrs_field( init=False, factory=dict ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_date_time_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_date_time_property.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_date_time_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_date_time_property.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_discriminated_union.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_discriminated_union.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_discriminated_union.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_discriminated_union.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties_string_to_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties_string_to_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties_string_to_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties_string_to_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_no_properties.py similarity index 97% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_no_properties.py index c74887f5b..32eba322b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_no_properties.py @@ -10,8 +10,6 @@ @_attrs_define class ModelWithNoProperties: - """ """ - def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py index ce4be38c0..f5a15805d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py @@ -12,8 +12,6 @@ @_attrs_define class ModelWithPrimitiveAdditionalPropertiesADateHolder: - """ """ - additional_properties: dict[str, datetime.datetime] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_property_ref.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_property_ref.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py index 1b7b4d5ea..f31d2f88d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py @@ -11,8 +11,6 @@ @_attrs_define class ModelWithRecursiveRefInAdditionalProperties: - """ """ - additional_properties: dict[str, ModelWithRecursiveRefInAdditionalProperties] = _attrs_field( init=False, factory=dict ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_apples.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_apples.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_apples.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_apples.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_bananas.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_bananas.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_bananas.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_bananas.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/none.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/none.py similarity index 98% rename from end_to_end_tests/golden-record/my_test_api_client/models/none.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/none.py index a6956e860..6f7fa6047 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/none.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/none.py @@ -11,8 +11,6 @@ @_attrs_define class None_: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/optional_body_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/optional_body_body.py similarity index 98% rename from end_to_end_tests/golden-record/my_test_api_client/models/optional_body_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/optional_body_body.py index 1a214a7ad..064beb792 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/optional_body_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/optional_body_body.py @@ -11,8 +11,6 @@ @_attrs_define class OptionalBodyBody: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_data_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_data_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_data_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_data_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_files_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_files_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_json_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_json_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_json_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_json_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_form_data_inline_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_form_data_inline_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_form_data_inline_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_form_data_inline_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py similarity index 99% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py index abceb221c..2b6169b11 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py @@ -11,8 +11,6 @@ @_attrs_define class PostResponsesUnionsSimpleBeforeComplexResponse200AType1: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_2xx.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_2xx.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_2xx_status.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx_status.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_2xx_status.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx_status.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_4xx.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_4xx.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_4xx.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_4xx.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_body.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_body.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_response_200.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_response_200.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/validation_error.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/validation_error.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/py.typed b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/py.typed similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/py.typed rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/py.typed diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/types.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/types.py diff --git a/end_to_end_tests/golden-record/pyproject.toml b/end_to_end_tests/golden-records/my-test-api-client/pyproject.toml similarity index 100% rename from end_to_end_tests/golden-record/pyproject.toml rename to end_to_end_tests/golden-records/my-test-api-client/pyproject.toml diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/.gitignore b/end_to_end_tests/golden-records/test-3-1-features-client/.gitignore new file mode 100644 index 000000000..79a2c3d73 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +build/ +dist/ +*.egg-info/ +.pytest_cache/ + +# pyenv +.python-version + +# Environments +.env +.venv + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# JetBrains +.idea/ + +/coverage.xml +/.coverage diff --git a/end_to_end_tests/test-3-1-golden-record/README.md b/end_to_end_tests/golden-records/test-3-1-features-client/README.md similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/README.md rename to end_to_end_tests/golden-records/test-3-1-features-client/README.md diff --git a/end_to_end_tests/test-3-1-golden-record/pyproject.toml b/end_to_end_tests/golden-records/test-3-1-features-client/pyproject.toml similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/pyproject.toml rename to end_to_end_tests/golden-records/test-3-1-features-client/pyproject.toml diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/__init__.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/__init__.py diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py new file mode 100644 index 000000000..81f9fa241 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/post_const_path.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/post_const_path.py diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/post_prefix_items.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/post_prefix_items.py diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py new file mode 100644 index 000000000..edb246d7e --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py @@ -0,0 +1,268 @@ +import ssl +from typing import Any, Self + +import httpx +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> Self: + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self: + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> Self: + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self: + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py new file mode 100644 index 000000000..5f92e76ac --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/__init__.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/__init__.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_const_path_body.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_const_path_body.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_const_path_body.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_const_path_body.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_prefix_items_body.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_prefix_items_body.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_prefix_items_body.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_prefix_items_body.py diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed new file mode 100644 index 000000000..1aad32711 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 \ No newline at end of file diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py new file mode 100644 index 000000000..b64af0952 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/regen_golden_record.py b/end_to_end_tests/regen_golden_record.py index e01a84884..425775530 100644 --- a/end_to_end_tests/regen_golden_record.py +++ b/end_to_end_tests/regen_golden_record.py @@ -1,4 +1,5 @@ -""" Regenerate golden-record """ +"""Regenerate golden-record""" + import filecmp import shutil from pathlib import Path @@ -11,20 +12,19 @@ def _regenerate( *, spec_file_name: str, - output_dir: str = "my-test-api-client", - golden_record_dir: str | None = None, + output_dir: str, + capture_golden_record: bool = False, config_file_name: str = "config.yml", - extra_args: list[str] | None = None + extra_args: list[str] | None = None, ) -> None: end_to_end_tests_base_path = Path(__file__).parent - project_base_path = end_to_end_tests_base_path.parent runner = CliRunner() openapi_path = end_to_end_tests_base_path / spec_file_name - output_path = project_base_path / output_dir + output_path = end_to_end_tests_base_path / "tmp" / output_dir shutil.rmtree(output_path, ignore_errors=True) - args = ["generate", f"--path={openapi_path}"] + args = ["generate", f"--path={openapi_path}", f"--output-path={output_path}"] if config_file_name: config_path = end_to_end_tests_base_path / config_file_name args.append(f"--config={config_path}") @@ -38,8 +38,8 @@ def _regenerate( print(result.stdout) if result.exception: raise Exception(f"{result.exception} {result.stderr}") - if golden_record_dir: - gr_path = end_to_end_tests_base_path / golden_record_dir + if capture_golden_record: + gr_path = end_to_end_tests_base_path / "golden-records" / output_dir shutil.rmtree(gr_path, ignore_errors=True) output_path.rename(gr_path) @@ -47,7 +47,8 @@ def _regenerate( def regen_golden_record(): _regenerate( spec_file_name="baseline_openapi_3.0.json", - golden_record_dir="golden-record", + output_dir="my-test-api-client", + capture_golden_record=True, ) @@ -55,7 +56,7 @@ def regen_golden_record_3_1_features(): _regenerate( spec_file_name="3.1_specific.openapi.yaml", output_dir="test-3-1-features-client", - golden_record_dir="test-3-1-golden-record", + capture_golden_record=True, ) @@ -63,16 +64,29 @@ def regen_literal_enums_golden_record(): _regenerate( spec_file_name="openapi_3.1_enums.yaml", output_dir="my-enum-api-client", - golden_record_dir="literal-enums-golden-record", + capture_golden_record=True, config_file_name="literal_enums.config.yml", ) +def regen_escapes_golden_record(): + _regenerate( + spec_file_name="escapes.openapi.yaml", + output_dir="escapes-client", + capture_golden_record=True, + ) + + def regen_metadata_snapshots(): - output_path = Path.cwd() / "test-3-1-features-client" + output_path = Path(__file__).parent / "tmp" / "test-3-1-features-client" snapshots_dir = Path(__file__).parent / "metadata_snapshots" - for (meta, file, rename_to) in (("setup", "setup.py", "setup.py"), ("pdm", "pyproject.toml", "pdm.pyproject.toml"), ("poetry", "pyproject.toml", "poetry.pyproject.toml"), ("uv", "pyproject.toml", "uv.pyproject.toml")): + for meta, file, rename_to in ( + ("setup", "setup.py", "setup.py"), + ("pdm", "pyproject.toml", "pdm.pyproject.toml"), + ("poetry", "pyproject.toml", "poetry.pyproject.toml"), + ("uv", "pyproject.toml", "uv.pyproject.toml"), + ): _regenerate( spec_file_name="3.1_specific.openapi.yaml", output_dir="test-3-1-features-client", @@ -86,7 +100,8 @@ def regen_metadata_snapshots(): def regen_docstrings_on_attributes_golden_record(): _regenerate( spec_file_name="docstrings_on_attributes.yml", - golden_record_dir="docstrings-on-attributes-golden-record", + output_dir="docstrings-on-attributes-golden-record", + capture_golden_record=True, config_file_name="docstrings_on_attributes.config.yml", ) @@ -96,13 +111,14 @@ def regen_custom_template_golden_record(): openapi_path = Path(__file__).parent / "baseline_openapi_3.0.json" tpl_dir = Path(__file__).parent / "test_custom_templates" - gr_path = Path(__file__).parent / "golden-record" - tpl_gr_path = Path(__file__).parent / "custom-templates-golden-record" + gr_path = Path(__file__).parent / "golden-records" / "my-test-api-client" + tpl_gr_path = Path(__file__).parent / "golden-records" / "custom-templates" - output_path = Path.cwd() / "my-test-api-client" + output_path = Path(__file__).parent / "tmp" / "my-test-api-client" config_path = Path(__file__).parent / "config.yml" shutil.rmtree(tpl_gr_path, ignore_errors=True) + shutil.rmtree(output_path, ignore_errors=True) result = runner.invoke( app, @@ -111,6 +127,7 @@ def regen_custom_template_golden_record(): f"--config={config_path}", f"--path={openapi_path}", f"--custom-template-path={tpl_dir}", + f"--output-path={output_path}", ], ) @@ -146,3 +163,4 @@ def regen_custom_template_golden_record(): regen_docstrings_on_attributes_golden_record() regen_custom_template_golden_record() regen_literal_enums_golden_record() + regen_escapes_golden_record() diff --git a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja index dd64d942c..aa207c418 100644 --- a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja +++ b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja @@ -13,11 +13,11 @@ class {{ class_name(endpoint_collection.tag) }}Endpoints: def {{ python_identifier(endpoint.name) }}(cls) -> types.ModuleType: {% if endpoint.description %} """ - {{ endpoint.description }} + {{ endpoint.description | safe_for_docstring }} """ {% elif endpoint.summary %} """ - {{ endpoint.summary }} + {{ endpoint.summary | safe_for_docstring }} """ {% endif %} return {{ python_identifier(endpoint.name) }} diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index 4e463670f..aa2bb6423 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -8,7 +8,8 @@ from typer.testing import CliRunner, Result from end_to_end_tests.generated_client import ( - _run_command, generate_client, generate_client_from_inline_spec, + _run_command, + generate_client, ) from openapi_python_client.cli import app @@ -18,7 +19,7 @@ def _compare_directories( test_subject: Path, expected_differences: dict[Path, str] | None = None, expected_missing: set[str] | None = None, - ignore: list[str] = None, + ignore: list[str] | None = None, depth=0, ): """ @@ -41,9 +42,7 @@ def _compare_directories( ) expected_differences = expected_differences or {} - _, mismatches, errors = cmpfiles( - record, test_subject, dc.common_files, shallow=False - ) + _, mismatches, _errors = cmpfiles(record, test_subject, dc.common_files, shallow=False) mismatches = set(mismatches) for file_name in mismatches: @@ -56,9 +55,7 @@ def _compare_directories( expected_content = (record / file_name).read_text() generated_content = (test_subject / file_name).read_text() - assert ( - generated_content == expected_content - ), f"Unexpected output in {mismatch_file_path}" + assert generated_content == expected_content, f"Unexpected output in {mismatch_file_path}" for sub_path in dc.common_dirs: _compare_directories( @@ -70,12 +67,7 @@ def _compare_directories( ) if depth == 0 and len(expected_differences.keys()) > 0: - failure = "\n".join( - [ - f"Expected {path} to be different but it was not" - for path in expected_differences.keys() - ] - ) + failure = "\n".join([f"Expected {path} to be different but it was not" for path in expected_differences.keys()]) pytest.fail(failure, pytrace=False) @@ -83,19 +75,20 @@ def run_e2e_test( openapi_document: str, extra_args: list[str], expected_differences: dict[Path, str] | None = None, - golden_record_path: str = "golden-record", output_path: str = "my-test-api-client", expected_missing: set[str] | None = None, - specify_output_path_explicitly: bool = True, + *, + golden_record_path: str | None = None, ) -> Result: - with generate_client(openapi_document, extra_args, output_path, specify_output_path_explicitly=specify_output_path_explicitly) as g: - gr_path = Path(__file__).parent / golden_record_path + with generate_client(openapi_document, extra_args, output_path) as g: + if golden_record_path: + gr_path = Path(__file__).parent / "golden-records" / golden_record_path + else: + gr_path = Path(__file__).parent / "golden-records" / output_path expected_differences = expected_differences or {} # Use absolute paths for expected differences for easier comparisons - expected_differences = { - g.output_path.joinpath(key): value for key, value in expected_differences.items() - } + expected_differences = {g.output_path.joinpath(key): value for key, value in expected_differences.items()} _compare_directories( gr_path, g.output_path, expected_differences=expected_differences, expected_missing=expected_missing ) @@ -104,6 +97,7 @@ def run_e2e_test( [sys.executable, "-m", "mypy", str(g.output_path), "--strict"], capture_output=True, text=True, + check=True, ) assert result.returncode == 0, f"Type checking client failed: {result.stdout}" @@ -123,19 +117,21 @@ def test_3_1_specific_features(): "3.1_specific.openapi.yaml", [], {}, - "test-3-1-golden-record", "test-3-1-features-client", ) def test_literal_enums_end_to_end(): config_path = Path(__file__).parent / "literal_enums.config.yml" + run_e2e_test("openapi_3.1_enums.yaml", [f"--config={config_path}"], {}, "my-enum-api-client") + + +def test_escapes_end_to_end(): run_e2e_test( - "openapi_3.1_enums.yaml", - [f"--config={config_path}"], + "escapes.openapi.yaml", + [], {}, - "literal-enums-golden-record", - "my-enum-api-client" + "escapes-client", ) @@ -146,7 +142,7 @@ def test_literal_enums_end_to_end(): ("pdm", "pyproject.toml", "pdm.pyproject.toml"), ("poetry", "pyproject.toml", "poetry.pyproject.toml"), ("uv", "pyproject.toml", "uv.pyproject.toml"), - ) + ), ) def test_meta(meta: str, generated_file: str | None, expected_file: str | None): with generate_client( @@ -156,20 +152,18 @@ def test_meta(meta: str, generated_file: str | None, expected_file: str | None): ) as g: if generated_file and expected_file: assert (g.output_path / generated_file).exists() - assert ( - (g.output_path / generated_file).read_text() == - (Path(__file__).parent / "metadata_snapshots" / expected_file).read_text() - ) + assert (g.output_path / generated_file).read_text() == ( + Path(__file__).parent / "metadata_snapshots" / expected_file + ).read_text() def test_none_meta(): run_e2e_test( "3.1_specific.openapi.yaml", ["--meta=none"], - golden_record_path="test-3-1-golden-record/test_3_1_features_client", + golden_record_path="test-3-1-features-client/test_3_1_features_client", output_path="test_3_1_features_client", expected_missing={"py.typed"}, - specify_output_path_explicitly=False, ) @@ -184,14 +178,10 @@ def test_docstrings_on_attributes(): def test_custom_templates(): - expected_differences = ( - {} - ) # key: path relative to generated directory, value: expected generated content + expected_differences = {} # key: path relative to generated directory, value: expected generated content api_dir = Path("my_test_api_client").joinpath("api") models_dir = Path("my_test_api_client").joinpath("models") - golden_tpls_root_dir = Path(__file__).parent.joinpath( - "custom-templates-golden-record" - ) + golden_tpls_root_dir = Path(__file__).parent / "golden-records" / "custom-templates" expected_difference_paths = [ Path("README.md"), @@ -200,9 +190,7 @@ def test_custom_templates(): ] for expected_difference_path in expected_difference_paths: - expected_differences[expected_difference_path] = ( - golden_tpls_root_dir / expected_difference_path - ).read_text() + expected_differences[expected_difference_path] = (golden_tpls_root_dir / expected_difference_path).read_text() # Each API module (defined by tag) has a custom __init__.py in it now. for endpoint_mod in golden_tpls_root_dir.joinpath(api_dir).iterdir(): @@ -281,13 +269,14 @@ def test_update_integration_tests(): "generate", extra_args=["--overwrite", "--meta=pdm", f"--output-path={temp_dir}"], url=url, - config_path=config_path + config_path=config_path, ) _compare_directories(source_path, temp_dir, ignore=["pyproject.toml"]) result = subprocess.run( [sys.executable, "-m", "mypy", str(temp_dir), "--strict"], capture_output=True, text=True, + check=True, ) assert result.returncode == 0, f"Type checking client failed: {result.stdout=} {result.stderr=}" diff --git a/fuzz/test_codegen.py b/fuzz/test_codegen.py new file mode 100644 index 000000000..177eb4009 --- /dev/null +++ b/fuzz/test_codegen.py @@ -0,0 +1,349 @@ +import ast +import io +import json +import os +import subprocess +import tomllib +import warnings +from contextlib import redirect_stdout +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + +from hypothesis import given, settings +from hypothesis import strategies as st + +from openapi_python_client import Config, ErrorLevel, MetaType, generate +from openapi_python_client.config import ConfigFile +from openapi_python_client.schema import OpenAPI + +FUZZ_EXAMPLES = int(os.environ.get("OPENAPI_PYTHON_CLIENT_FUZZ_EXAMPLES", "100")) + +# Canary payload: if any untrusted string breaks out of its literal/docstring in generated code, +# this appears as a real AST Name/Call node instead of text inside a string constant. +CANARY = "PWNED_CANARY" +CALL_CANARY = "EXECUTED_CANARY" + +DANGEROUS_STRINGS = ( + "", + "'", + '"', + "\\", + "\n", + "\r\n", + "\t", + "\x00", + '"""', + "'''", + "{value!r}", + "${value}", + "{{ value }}", + "line one\\\nline two", + "\\U0000000", + "\\N{NOT A REAL NAME}", + "# type: ignore\n" + CANARY + "()", + "\n# comment with trailing slash\\", + "from os import system", + "\N{LINE SEPARATOR}", + "\N{PILE OF POO}", + f'"{CANARY}("', + f'"{CANARY}() + "', + f"'''{CANARY}()'''", + f'"""{CANARY}()"""', + f'\\"{CANARY}()', + f"\n{CANARY}()\n", + f'"; print("{CALL_CANARY}"); _fuzz = "', + f'"""\nprint("{CALL_CANARY}")\n"""', +) +UTF8_CHARACTERS = st.characters(blacklist_categories=["Cs"]) +UNTRUSTED_TEXT = st.one_of( + st.sampled_from(DANGEROUS_STRINGS), + st.text(UTF8_CHARACTERS, max_size=60), +) +NONEMPTY_UNTRUSTED_TEXT = st.one_of( + st.sampled_from(tuple(value for value in DANGEROUS_STRINGS if value)), + st.text(UTF8_CHARACTERS, min_size=1, max_size=40), +) +COMPONENT_NAME = st.one_of( + st.sampled_from(tuple(value for value in DANGEROUS_STRINGS if value and "/" not in value and "~" not in value)), + st.text(UTF8_CHARACTERS, min_size=1, max_size=30).filter(lambda value: "/" not in value and "~" not in value), +) + + +@st.composite +def schema_strategy(draw: st.DrawFn) -> dict[str, Any]: + description = draw(UNTRUSTED_TEXT) + kind = draw( + st.sampled_from( + ("string", "integer", "number", "boolean", "array", "object", "reference", "union", "const", "free_form") + ) + ) + + value: Any + schema: dict[str, Any] = {"description": description} + if kind == "string": + value = draw(UNTRUSTED_TEXT) + schema.update({"type": "string", "default": value, "example": value}) + elif kind == "integer": + value = draw(st.integers(min_value=-(2**31), max_value=2**31 - 1)) + schema.update({"type": "integer", "default": value, "example": value}) + elif kind == "number": + value = draw(st.floats(allow_nan=False, allow_infinity=False, width=32)) + schema.update({"type": "number", "default": value, "example": value}) + elif kind == "boolean": + value = draw(st.booleans()) + schema.update({"type": "boolean", "default": value, "example": value}) + elif kind == "array": + schema.update( + { + "type": "array", + "items": {"type": "string", "description": draw(UNTRUSTED_TEXT)}, + "example": [draw(UNTRUSTED_TEXT)], + } + ) + elif kind == "object": + schema.update( + { + "title": draw(UNTRUSTED_TEXT), + "type": "object", + "properties": {"nested": {"type": "string", "description": draw(UNTRUSTED_TEXT)}}, + "additionalProperties": {"type": "string", "description": draw(UNTRUSTED_TEXT)}, + "example": {"nested": draw(UNTRUSTED_TEXT)}, + } + ) + elif kind == "union": + schema = { + "oneOf": [ + {"type": "string", "description": description}, + {"type": "integer", "description": draw(UNTRUSTED_TEXT)}, + ], + "description": draw(UNTRUSTED_TEXT), + } + elif kind == "const": + schema = {"const": draw(UNTRUSTED_TEXT), "description": description} + elif kind == "free_form": + schema = {"type": "object", "additionalProperties": True, "description": description} + else: + schema = {"$ref": "#/components/schemas/Choice"} + return schema + + +@st.composite +def openapi_documents(draw: st.DrawFn) -> dict[str, Any]: + component_name = draw(COMPONENT_NAME) + property_name = draw(NONEMPTY_UNTRUSTED_TEXT) + query_name = draw(NONEMPTY_UNTRUSTED_TEXT) + header_name = draw(NONEMPTY_UNTRUSTED_TEXT) + cookie_name = draw(NONEMPTY_UNTRUSTED_TEXT) + path_segment = draw(NONEMPTY_UNTRUSTED_TEXT) + security_name = draw(NONEMPTY_UNTRUSTED_TEXT) + property_schema = draw(schema_strategy()) + parameter_schema = draw(schema_strategy()) + enum_values = draw(st.lists(UNTRUSTED_TEXT, min_size=1, max_size=4, unique=True)) + int_enum_values = draw(st.lists(st.integers(min_value=-100, max_value=100), min_size=1, max_size=4, unique=True)) + + return { + "openapi": draw(st.sampled_from(("3.0.0", "3.0.3", "3.1.0"))), + "info": { + "title": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "version": draw(UNTRUSTED_TEXT), + "termsOfService": draw(UNTRUSTED_TEXT), + "contact": { + "name": draw(UNTRUSTED_TEXT), + "url": draw(UNTRUSTED_TEXT), + "email": draw(UNTRUSTED_TEXT), + }, + "license": {"name": draw(NONEMPTY_UNTRUSTED_TEXT), "url": draw(UNTRUSTED_TEXT)}, + }, + "servers": [ + { + "url": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "variables": { + "fuzz": { + "default": draw(UNTRUSTED_TEXT), + "enum": [draw(UNTRUSTED_TEXT)], + "description": draw(UNTRUSTED_TEXT), + } + }, + } + ], + "tags": [{"name": draw(NONEMPTY_UNTRUSTED_TEXT), "description": draw(UNTRUSTED_TEXT)}], + "externalDocs": {"url": draw(UNTRUSTED_TEXT), "description": draw(UNTRUSTED_TEXT)}, + "security": [{security_name: []}], + "paths": { + f"/widgets/{{widget_id}}/{path_segment}": { + "summary": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "post": { + "operationId": draw(NONEMPTY_UNTRUSTED_TEXT), + "tags": [draw(UNTRUSTED_TEXT)], + "summary": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "parameters": [ + { + "name": "widget_id", + "in": "path", + "required": True, + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string"}, + }, + { + "name": query_name, + "in": "query", + "description": draw(UNTRUSTED_TEXT), + "schema": parameter_schema, + }, + { + "name": header_name, + "in": "header", + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string", "default": draw(UNTRUSTED_TEXT)}, + }, + { + "name": cookie_name, + "in": "cookie", + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string"}, + }, + ], + "requestBody": { + "required": True, + "description": draw(UNTRUSTED_TEXT), + "content": { + "application/json": { + "schema": {"$ref": f"#/components/schemas/{component_name}"}, + } + }, + }, + "responses": { + "200": { + "description": draw(UNTRUSTED_TEXT), + "headers": { + "X-Fuzz": { + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string"}, + } + }, + "content": { + "application/json": { + "schema": {"$ref": f"#/components/schemas/{component_name}"}, + } + }, + } + }, + }, + } + }, + "components": { + "securitySchemes": { + security_name: { + "type": "apiKey", + "name": draw(NONEMPTY_UNTRUSTED_TEXT), + "in": "header", + "description": draw(UNTRUSTED_TEXT), + } + }, + "schemas": { + component_name: { + "title": draw(UNTRUSTED_TEXT), + "type": "object", + "description": draw(UNTRUSTED_TEXT), + "externalDocs": {"url": draw(UNTRUSTED_TEXT), "description": draw(UNTRUSTED_TEXT)}, + "xml": { + "name": draw(UNTRUSTED_TEXT), + "namespace": draw(UNTRUSTED_TEXT), + "prefix": draw(UNTRUSTED_TEXT), + }, + "required": [property_name], + "properties": {property_name: property_schema}, + }, + "Choice": { + "type": "string", + "description": draw(UNTRUSTED_TEXT), + "enum": enum_values, + }, + "IntChoice": { + "type": "integer", + "description": draw(UNTRUSTED_TEXT), + "enum": int_enum_values, + }, + }, + }, + } + + +@settings(max_examples=FUZZ_EXAMPLES, deadline=None) +@given(document=openapi_documents(), meta_type=st.sampled_from(list(MetaType))) +def test_valid_openapi_never_generates_invalid_python(document: dict[str, Any], meta_type: MetaType) -> None: + # The generator's own schema model is the source of truth for "valid": anything it accepts + # must produce syntactically valid Python. + OpenAPI.model_validate(document) + + with TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + document_path = root / "openapi.json" + output_path = root / "generated" + document_path.write_text(json.dumps(document, ensure_ascii=False), encoding="utf-8") + + config = Config.from_sources( + ConfigFile( + project_name_override="fuzz-client", + package_name_override="fuzz_client", + post_hooks=[], + ), + meta_type, + document_source=document_path, + file_encoding="utf-8", + overwrite=False, + output_path=output_path, + ) + with redirect_stdout(io.StringIO()): + errors = generate(config=config) + + fatal_errors = [error for error in errors if error.level == ErrorLevel.ERROR] + assert not fatal_errors + + python_files = sorted(output_path.rglob("*.py")) + assert python_files + for python_file in python_files: + source = python_file.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(python_file)) + with warnings.catch_warnings(): + warnings.simplefilter("error") + compile(source, str(python_file), "exec") + _assert_no_code_injection(tree, python_file) + + for pyproject_file in output_path.rglob("pyproject.toml"): + with pyproject_file.open("rb") as pyproject: + tomllib.load(pyproject) + + # Static analysis catches escapes which produce valid syntax but reference names that + # don't exist (e.g. an injected function call) — even ones the canary doesn't cover. + ruff = subprocess.run( + ["ruff", "check", "--select", "F821", "--no-cache", str(output_path)], + capture_output=True, + text=True, + check=False, + ) + assert ruff.returncode == 0, f"Generated code references undefined names:\n{ruff.stdout}" + + +def _assert_no_code_injection(tree: ast.AST, python_file: Path) -> None: + """Fail if the canary escaped a string literal/docstring and became executable code.""" + for node in ast.walk(tree): + if isinstance(node, ast.Name) and CANARY in node.id: + raise AssertionError(f"Canary escaped into a name in {python_file}: {node.id}") + if isinstance(node, ast.Attribute) and CANARY in node.attr: + raise AssertionError(f"Canary escaped into an attribute in {python_file}: {node.attr}") + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and CANARY in node.name: + raise AssertionError(f"Canary escaped into a definition in {python_file}: {node.name}") + if isinstance(node, ast.ImportFrom) and node.module and CANARY in node.module: + raise AssertionError(f"Canary escaped into an import in {python_file}: {node.module}") + if isinstance(node, ast.alias) and CANARY in node.name: + raise AssertionError(f"Canary escaped into an import in {python_file}: {node.name}") + if isinstance(node, ast.Call) and any( + isinstance(child, ast.Constant) and child.value == CALL_CANARY for child in ast.walk(node) + ): + raise AssertionError(f"Built-in call canary became executable code in {python_file}") diff --git a/knope.toml b/knope.toml index 7859e93b5..53b453bf4 100644 --- a/knope.toml +++ b/knope.toml @@ -2,7 +2,11 @@ versioned_files = ["pyproject.toml"] changelog = "CHANGELOG.md" extra_changelog_sections = [ - { name = "Notes", types = ["note"] } + { types = ["security"], name = "🚨Security" }, + { types = ["major"], name = "❗️Breaking ❗" }, + { types = ["minor"], name = "🚀 Features" }, + { types = ["patch"], name = "🐛 Fixes" }, + { types = ["note"], name = "📝Notes" } ] [github] diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index cb19d2947..c3b766338 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -4,7 +4,7 @@ import mimetypes import shutil import subprocess -from collections.abc import Sequence +from collections.abc import Callable, Sequence from importlib.metadata import version from pathlib import Path from subprocess import CalledProcessError @@ -16,7 +16,7 @@ from ruamel.yaml import YAML from ruamel.yaml.error import YAMLError -from openapi_python_client import utils +from openapi_python_client import strings from .config import Config, MetaType from .parser import GeneratorData, import_string_from_class @@ -25,11 +25,16 @@ __version__ = version(__package__) +from .schema.untrusted_string import UntrustedString -TEMPLATE_FILTERS = { - "snakecase": utils.snake_case, - "kebabcase": utils.kebab_case, - "pascalcase": utils.pascal_case, +TEMPLATE_FILTERS: dict[str, Callable[..., object]] = { + "snakecase": strings.snake_case, + "kebabcase": strings.kebab_case, + "pascalcase": strings.pascal_case, + "safe_for_docstring": strings.safe_for_docstring, + "in_double_quote_literal": strings.in_double_quote_literal, + "in_f_string_literal": strings.in_f_string_literal, + "as_unembedded_code": strings.PythonCode.as_unembedded_code, "any": any, } @@ -66,14 +71,14 @@ def __init__( keep_trailing_newline=True, ) - self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client" + self.project_name: str = config.project_name_override or f"{strings.kebab_case(openapi.title).lower()}-client" self.package_name: str = config.package_name_override or self.project_name.replace("-", "_") self.project_dir: Path # Where the generated code will be placed self.package_dir: Path # Where the generated Python module will be placed (same as project_dir if no meta) if config.output_path is not None: self.project_dir = config.output_path - elif config.meta_type == MetaType.NONE: + elif config.meta_type == MetaType.NONE: # pragma: no cover self.project_dir = Path.cwd() / self.package_name else: self.project_dir = Path.cwd() / self.project_name @@ -83,17 +88,20 @@ def __init__( else: self.package_dir = self.project_dir / self.package_name - self.package_description: str = utils.remove_string_escapes( - f"A client library for accessing {self.openapi.title}" + self.package_description = UntrustedString( + f"A client library for accessing {self.openapi.title.get_untrusted_value()}" + ) + self.version: UntrustedString = ( + UntrustedString(config.package_version_override) if config.package_version_override else openapi.version ) - self.version: str = config.package_version_override or openapi.version self.env.filters.update(TEMPLATE_FILTERS) self.env.globals.update( config=config, - utils=utils, - python_identifier=lambda x: utils.PythonIdentifier(x, config.field_prefix), - class_name=lambda x: utils.ClassName(x, config.field_prefix), + strings=strings, + PythonCode=strings.PythonCode, + python_identifier=lambda x: strings.PythonIdentifier(x, config.field_prefix), + class_name=lambda x: strings.ClassName(x, config.field_prefix), package_name=self.package_name, package_dir=self.package_dir, package_description=self.package_description, @@ -110,7 +118,7 @@ def build(self) -> Sequence[GeneratorError]: print(f"Generating {self.project_dir}") try: - self.project_dir.mkdir() + self.project_dir.mkdir(parents=True) except FileExistsError: if not self.config.overwrite: return [GeneratorError(detail="Directory already exists. Delete it or use the --overwrite option.")] @@ -267,7 +275,8 @@ def _build_api(self) -> None: endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag endpoint_template = self.env.get_template( - "endpoint_module.py.jinja", globals={"isbool": lambda obj: obj.get_base_type_string() == "bool"} + "endpoint_module.py.jinja", + globals={"isbool": lambda obj: obj.get_base_type_string() == strings.PythonCode("bool")}, ) for tag, collection in endpoint_collections_by_tag.items(): tag_dir = api_dir / tag @@ -281,7 +290,7 @@ def _build_api(self) -> None: ) for endpoint in collection.endpoints: - module_path = tag_dir / f"{utils.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py" + module_path = tag_dir / f"{strings.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py" module_path.write_text( endpoint_template.render( endpoint=endpoint, diff --git a/openapi_python_client/cli.py b/openapi_python_client/cli.py index 3972703ae..863ebbb5a 100644 --- a/openapi_python_client/cli.py +++ b/openapi_python_client/cli.py @@ -1,7 +1,6 @@ import codecs from collections.abc import Sequence from pathlib import Path -from pprint import pformat import typer @@ -75,7 +74,7 @@ def _print_parser_error(err: GeneratorError, color: str) -> None: typer.echo(err=True) if isinstance(err, ParseError) and err.data is not None: - formatted_data = pformat(err.data) + formatted_data = err.data.model_dump_json(indent=2, exclude_unset=True, by_alias=True) typer.secho(formatted_data, fg=color, err=True) typer.echo(err=True) diff --git a/openapi_python_client/config.py b/openapi_python_client/config.py index a30d2ddbf..61cf45c8d 100644 --- a/openapi_python_client/config.py +++ b/openapi_python_client/config.py @@ -7,6 +7,8 @@ from pydantic import BaseModel from ruamel.yaml import YAML +from openapi_python_client.schema.untrusted_string import UntrustedString + class ClassOverride(BaseModel): """An override of a single generated class. @@ -14,7 +16,7 @@ class ClassOverride(BaseModel): See https://github.com/openapi-generators/openapi-python-client#class_overrides """ - class_name: str | None = None + class_name: UntrustedString | None = None module_name: str | None = None diff --git a/openapi_python_client/parser/bodies.py b/openapi_python_client/parser/bodies.py index 41ad8a968..ae6e30fd9 100644 --- a/openapi_python_client/parser/bodies.py +++ b/openapi_python_client/parser/bodies.py @@ -12,7 +12,8 @@ from .. import schema as oai from ..config import Config -from ..utils import get_content_type +from ..schema.untrusted_string import UntrustedString +from ..strings import get_content_type from .errors import ErrorLevel, ParseError @@ -25,7 +26,7 @@ class BodyType(StrEnum): @attr.define class Body: - content_type: str + content_type: UntrustedString prop: Property body_type: BodyType @@ -34,7 +35,7 @@ def body_from_data( *, data: oai.Operation, schemas: Schemas, - request_bodies: dict[str, oai.RequestBody | oai.Reference], + request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference], config: Config, endpoint_name: str, ) -> tuple[list[Body | ParseError], Schemas]: @@ -50,7 +51,7 @@ def body_from_data( prefix_type_names = len(body_content) > 1 for content_type, media_type in body_content.items(): - simplified_content_type = get_content_type(content_type, config) + simplified_content_type = get_content_type(content_type.get_untrusted_value(), config) if simplified_content_type is None: bodies.append( ParseError( @@ -88,7 +89,7 @@ def body_from_data( ) continue prop, schemas = property_from_data( - name="body", + name=UntrustedString("body"), required=body.required, data=media_type_schema, schemas=schemas, @@ -121,7 +122,7 @@ def body_from_data( def _resolve_reference( - body: oai.RequestBody | oai.Reference | None, request_bodies: dict[str, oai.RequestBody | oai.Reference] + body: oai.RequestBody | oai.Reference | None, request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference] ) -> oai.RequestBody | ParseError | None: if body is None: return None @@ -132,5 +133,5 @@ def _resolve_reference( if isinstance(body, oai.Reference): return ParseError(detail="Circular $ref in request body", data=body) if body is None and references_seen: - return ParseError(detail=f"Could not resolve $ref {references_seen[-1]} in request body") + return ParseError(detail=f"Could not resolve $ref {references_seen[-1].get_untrusted_value()} in request body") return body diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 4f83ae93e..c16f03f2f 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -7,9 +7,10 @@ from pydantic import ValidationError from .. import schema as oai -from .. import utils +from .. import strings from ..config import Config -from ..utils import PythonIdentifier +from ..schema.untrusted_string import UntrustedString +from ..strings import PythonIdentifier from .bodies import Body, body_from_data from .errors import GeneratorError, ParseError, PropertyError from .properties import ( @@ -46,15 +47,15 @@ class EndpointCollection: @staticmethod def from_data( *, - data: dict[str, oai.PathItem], + data: dict[UntrustedString, oai.PathItem], schemas: Schemas, parameters: Parameters, - request_bodies: dict[str, oai.RequestBody | oai.Reference], - responses: dict[str, oai.Response | oai.Reference], + request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference], + responses: dict[UntrustedString, oai.Response | oai.Reference], config: Config, - ) -> tuple[dict[utils.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]: + ) -> tuple[dict[strings.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]: """Parse the openapi paths data to get EndpointCollections by tag""" - endpoints_by_tag: dict[utils.PythonIdentifier, EndpointCollection] = {} + endpoints_by_tag: dict[strings.PythonIdentifier, EndpointCollection] = {} methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] @@ -64,7 +65,10 @@ def from_data( if operation is None: continue - tags = [utils.PythonIdentifier(value=tag, prefix="tag") for tag in operation.tags or ["default"]] + tags = [ + strings.PythonIdentifier(value=tag, prefix="tag") + for tag in operation.tags or [UntrustedString("default")] + ] if not config.generate_all_tags: tags = tags[:1] @@ -93,12 +97,14 @@ def from_data( if not isinstance(endpoint, ParseError): endpoint = Endpoint.sort_parameters(endpoint=endpoint) if isinstance(endpoint, ParseError): - endpoint.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated." + endpoint.header = f"WARNING parsing {method.upper()} {path.get_untrusted_value()} within {'/'.join(tags)}. Endpoint will not be generated." for collection in collections: collection.parse_errors.append(endpoint) continue for error in endpoint.errors: - error.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}." + error.header = ( + f"WARNING parsing {method.upper()} {path.get_untrusted_value()} within {'/'.join(tags)}." + ) for collection in collections: collection.parse_errors.append(error) for collection in collections: @@ -107,14 +113,14 @@ def from_data( return endpoints_by_tag, schemas, parameters -def generate_operation_id(*, path: str, method: str) -> str: +def generate_operation_id(*, path: UntrustedString, method: str, prefix: str) -> PythonIdentifier: """Generate an operationId from a path""" - clean_path = path.replace("{", "").replace("}", "").replace("/", "_") - if clean_path.startswith("_"): - clean_path = clean_path[1:] - if clean_path.endswith("_"): - clean_path = clean_path[:-1] - return f"{method}_{clean_path}" + unsafe_id = path.get_untrusted_value().replace("{", "").replace("}", "").replace("/", "_") + if unsafe_id.startswith("_"): + unsafe_id = unsafe_id[1:] + if unsafe_id.endswith("_"): # pragma: no cover + unsafe_id = unsafe_id[:-1] + return PythonIdentifier(f"{method}_{unsafe_id}", prefix=prefix) models_relative_prefix: str = "..." @@ -134,13 +140,13 @@ class Endpoint: Describes a single endpoint on the server """ - path: str + path: UntrustedString method: str - description: str | None - name: str + description: UntrustedString + name: PythonIdentifier requires_security: bool tags: list[PythonIdentifier] - summary: str | None = "" + summary: UntrustedString relative_imports: set[str] = field(default_factory=set) query_parameters: list[Property] = field(default_factory=list) path_parameters: list[Property] = field(default_factory=list) @@ -156,7 +162,7 @@ def _add_responses( endpoint: "Endpoint", data: oai.Responses, schemas: Schemas, - responses: dict[str, oai.Response | oai.Reference], + responses: dict[UntrustedString, oai.Response | oai.Reference], config: Config, ) -> tuple["Endpoint", Schemas]: endpoint = deepcopy(endpoint) @@ -234,7 +240,7 @@ def add_parameters( endpoint = deepcopy(endpoint) - unique_parameters: set[tuple[str, oai.ParameterLocation]] = set() + unique_parameters: set[tuple[UntrustedString, oai.ParameterLocation]] = set() parameters_by_location: dict[str, list[Property]] = { oai.ParameterLocation.QUERY: endpoint.query_parameters, oai.ParameterLocation.PATH: endpoint.path_parameters, @@ -260,7 +266,7 @@ def add_parameters( detail=( "Parameters MUST NOT contain duplicates. " "A unique parameter is defined by a combination of a name and location. " - f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`." + f"Duplicated parameters named `{param.name.get_untrusted_value()}` detected in `{param.param_in}`." ), ), schemas, @@ -312,7 +318,7 @@ def _check_parameters_for_conflicts( self, *, config: Config, - previously_modified_params: set[tuple[oai.ParameterLocation, str]] | None = None, + previously_modified_params: set[tuple[oai.ParameterLocation, UntrustedString]] | None = None, ) -> "Endpoint | ParseError": """Check for conflicting parameters @@ -330,7 +336,7 @@ def _check_parameters_for_conflicts( location, prop = parameter if prop.python_name in reserved_names: - prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config) + prop.set_python_name(new_name=UntrustedString(f"{prop.python_name}_{location}"), config=config) modified_params.add((location, prop.name)) continue @@ -349,9 +355,9 @@ def _check_parameters_for_conflicts( if location != conflicting_location: conflicting_prop.set_python_name( - new_name=f"{conflicting_prop.python_name}_{conflicting_location}", config=config + new_name=UntrustedString(f"{conflicting_prop.python_name}_{conflicting_location}"), config=config ) - prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config) + prop.set_python_name(new_name=UntrustedString(f"{prop.python_name}_{location}"), config=config) elif conflicting_prop.name != prop.name: # Use the name to differentiate conflicting_prop.set_python_name(new_name=conflicting_prop.name, config=config, skip_snake_case=True) prop.set_python_name(new_name=prop.name, config=config, skip_snake_case=True) @@ -378,7 +384,7 @@ def sort_parameters(*, endpoint: "Endpoint") -> "Endpoint | ParseError": the path parameters and they could not be sorted. """ endpoint = deepcopy(endpoint) - parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path) + parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path.get_untrusted_value()) try: endpoint.path_parameters.sort( key=lambda param: parameters_from_path.index(param.name), @@ -388,37 +394,41 @@ def sort_parameters(*, endpoint: "Endpoint") -> "Endpoint | ParseError": if parameters_from_path != [param.name for param in endpoint.path_parameters]: return ParseError( - detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)", + detail=f"Incorrect path templating for {endpoint.path.get_untrusted_value()} (Path parameters do not match with path)", ) for parameter in endpoint.path_parameters: - endpoint.path = endpoint.path.replace(f"{{{parameter.name}}}", f"{{{parameter.python_name}}}") + endpoint.path = UntrustedString( + endpoint.path.get_untrusted_value().replace( + f"{{{parameter.name.get_untrusted_value()}}}", f"{{{parameter.python_name}}}" + ) + ) return endpoint @staticmethod def from_data( *, data: oai.Operation, - path: str, + path: UntrustedString, method: str, tags: list[PythonIdentifier], schemas: Schemas, parameters: Parameters, - request_bodies: dict[str, oai.RequestBody | oai.Reference], - responses: dict[str, oai.Response | oai.Reference], + request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference], + responses: dict[UntrustedString, oai.Response | oai.Reference], config: Config, ) -> tuple["Endpoint | ParseError", Schemas, Parameters]: """Construct an endpoint from the OpenAPI data""" if data.operationId is None: - name = generate_operation_id(path=path, method=method) + name = generate_operation_id(path=path, method=method, prefix=config.field_prefix) else: - name = data.operationId + name = PythonIdentifier(data.operationId, prefix=config.field_prefix) endpoint = Endpoint( path=path, method=method, - summary=utils.remove_string_escapes(data.summary) if data.summary else "", - description=utils.remove_string_escapes(data.description) if data.description else "", + summary=data.summary if data.summary else UntrustedString(""), + description=data.description if data.description else UntrustedString(""), name=name, requires_security=bool(data.security), tags=tags, @@ -440,7 +450,7 @@ def from_data( responses=responses, config=config, ) - if isinstance(result, ParseError): + if isinstance(result, ParseError): # pragma: no cover return result, schemas, parameters bodies, schemas = body_from_data( data=data, schemas=schemas, config=config, endpoint_name=result.name, request_bodies=request_bodies @@ -467,14 +477,14 @@ def from_data( return result, schemas, parameters - def response_type(self) -> str: + def response_type(self) -> strings.PythonCode: """Get the Python type of any response from this endpoint""" - types = sorted({response.prop.get_type_string() for response in self.responses}) + types = sorted({response.prop.get_type_string().as_unembedded_code() for response in self.responses}) if len(types) == 0: - return "Any" + return strings.PythonCode("Any") if len(types) == 1: - return types[0] - return " | ".join(types) + return strings.PythonCode(types[0]) + return strings.PythonCode(" | ".join(types)) def iter_all_parameters(self) -> Iterator[tuple[oai.ParameterLocation, Property]]: """Iterate through all the parameters of this endpoint""" @@ -498,12 +508,12 @@ def list_all_parameters(self) -> list[Property]: class GeneratorData: """All the data needed to generate a client""" - title: str - description: str | None - version: str + title: UntrustedString + description: UntrustedString | None + version: UntrustedString models: list[ModelProperty] errors: list[ParseError] - endpoint_collections_by_tag: dict[utils.PythonIdentifier, EndpointCollection] + endpoint_collections_by_tag: dict[strings.PythonIdentifier, EndpointCollection] enums: list[EnumProperty | LiteralEnumProperty] @staticmethod diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index ba81d27c6..09aab386b 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -18,8 +18,10 @@ from attrs import evolve -from ... import Config, utils +from ... import Config, strings from ... import schema as oai +from ...schema.ref import Ref +from ...schema.untrusted_string import UntrustedString from ..errors import ParameterError, ParseError, PropertyError from .any import AnyProperty from .boolean import BooleanProperty @@ -51,11 +53,11 @@ def _string_based_property( - name: str, required: bool, data: oai.Schema, config: Config + name: UntrustedString, required: bool, data: oai.Schema, config: Config ) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | UuidProperty | PropertyError: """Construct a Property from the type "string" """ string_format = data.schema_format - python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix) + python_name = strings.PythonIdentifier(value=name, prefix=config.field_prefix) if string_format == "date-time": return DateTimeProperty.build( name=name, @@ -103,16 +105,16 @@ def _string_based_property( def _property_from_ref( - name: str, + name: UntrustedString, required: bool, parent: oai.Schema | None, data: oai.Reference, schemas: Schemas, config: Config, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[Property | PropertyError, Schemas]: ref_path = parse_reference_path(data.ref) - if isinstance(ref_path, ParseError): + if isinstance(ref_path, ParseError): # pragma: no cover return PropertyError(data=data, detail=ref_path.detail), schemas existing = schemas.classes_by_reference.get(ref_path) if not existing: @@ -130,7 +132,7 @@ def _property_from_ref( existing, required=required, name=name, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), default=default, # type: ignore # mypy can't tell that default comes from the same class... ) @@ -139,18 +141,17 @@ def _property_from_ref( def property_from_data( # noqa: PLR0911, PLR0912 - name: str, + name: UntrustedString, required: bool, data: oai.Reference | oai.Schema, schemas: Schemas, parent_name: str, config: Config, process_properties: bool = True, - roots: set[ReferencePath | utils.ClassName] | None = None, + roots: set[ReferencePath | strings.ClassName] | None = None, ) -> tuple[Property | PropertyError, Schemas]: """Generate a Property from the OpenAPI dictionary representation of it""" roots = roots or set() - name = utils.remove_string_escapes(name) if isinstance(data, oai.Reference): return _property_from_ref( name=name, @@ -190,7 +191,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, required=required, default=data.default, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -230,7 +231,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 required=required, default=data.default, const=data.const, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, ), schemas, @@ -246,7 +247,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, default=data.default, required=required, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -258,7 +259,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, default=data.default, required=required, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -270,7 +271,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, required=required, default=None, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -303,7 +304,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, required=required, default=data.default, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -313,11 +314,11 @@ def property_from_data( # noqa: PLR0911, PLR0912 def _create_schemas( *, - components: dict[str, oai.Reference | oai.Schema], + components: dict[UntrustedString, oai.Reference | oai.Schema], schemas: Schemas, config: Config, ) -> Schemas: - to_process: Iterable[tuple[str, oai.Reference | oai.Schema]] = components.items() + to_process: Iterable[tuple[UntrustedString, oai.Reference | oai.Schema]] = components.items() still_making_progress = True errors: list[PropertyError] = [] @@ -325,12 +326,12 @@ def _create_schemas( while still_making_progress: still_making_progress = False errors = [] - next_round: list[tuple[str, oai.Reference | oai.Schema]] = [] + next_round: list[tuple[UntrustedString, oai.Reference | oai.Schema]] = [] # Only accumulate errors from the last round, since we might fix some along the way for name, data in to_process: schema_data: oai.Reference | oai.Schema | None = data - ref_path = parse_reference_path(f"#/components/schemas/{name}") - if isinstance(ref_path, ParseError): + ref_path = parse_reference_path(Ref(f"#/components/schemas/{name.get_untrusted_value()}")) + if isinstance(ref_path, ParseError): # pragma: no cover schemas.errors.append(PropertyError(detail=ref_path.detail, data=data)) continue if isinstance(data, oai.Reference): @@ -361,13 +362,13 @@ def _create_schemas( return schemas -def _propogate_removal(*, root: ReferencePath | utils.ClassName, schemas: Schemas, error: PropertyError) -> None: - if isinstance(root, utils.ClassName): +def _propogate_removal(*, root: ReferencePath | strings.ClassName, schemas: Schemas, error: PropertyError) -> None: + if isinstance(root, strings.ClassName): schemas.classes_by_name.pop(root, None) return if root in schemas.classes_by_reference: error.detail = error.detail or "" - error.detail += f"\n{root}" + error.detail += f"\n{root.get_untrusted_value()}" del schemas.classes_by_reference[root] for child in schemas.dependencies.get(root, set()): _propogate_removal(root=child, schemas=schemas, error=error) @@ -399,10 +400,10 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas: for model_prop in to_process: schemas_or_err = process_model(model_prop, schemas=schemas, config=config) if isinstance(schemas_or_err, PropertyError): - schemas_or_err.header = f"\nUnable to process schema {model_prop.name}:" + schemas_or_err.header = f"\nUnable to process schema {model_prop.name.get_untrusted_value()}:" if isinstance(schemas_or_err.data, oai.Reference) and schemas_or_err.data.ref.endswith( f"/{model_prop.class_info.name}" - ): + ): # pragma: no cover schemas_or_err.detail = schemas_or_err.detail or "" schemas_or_err.detail += "\n\nRecursive allOf reference found" final_model_errors.append((model_prop, schemas_or_err)) @@ -421,7 +422,7 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas: def build_schemas( *, - components: dict[str, oai.Reference | oai.Schema], + components: dict[UntrustedString, oai.Reference | oai.Schema], schemas: Schemas, config: Config, ) -> Schemas: @@ -433,12 +434,12 @@ def build_schemas( def build_parameters( *, - components: dict[str, oai.Reference | oai.Parameter], + components: dict[UntrustedString, oai.Reference | oai.Parameter], parameters: Parameters, config: Config, ) -> Parameters: """Get a list of Parameters from an OpenAPI dict""" - to_process: Iterable[tuple[str, oai.Reference | oai.Parameter]] = [] + to_process: Iterable[tuple[UntrustedString, oai.Reference | oai.Parameter]] = [] if components is not None: to_process = components.items() still_making_progress = True @@ -451,17 +452,17 @@ def build_parameters( next_round = [] # Only accumulate errors from the last round, since we might fix some along the way for name, data in to_process: - if isinstance(data, oai.Reference): + if isinstance(data, oai.Reference): # pragma: no cover parameters.errors.append(ParameterError(data=data, detail="Reference parameters are not supported.")) continue - ref_path = parse_reference_path(f"#/components/parameters/{name}") - if isinstance(ref_path, ParseError): + ref_path = parse_reference_path(Ref(f"#/components/parameters/{name.get_untrusted_value()}")) + if isinstance(ref_path, ParseError): # pragma: no cover parameters.errors.append(ParameterError(detail=ref_path.detail, data=data)) continue parameters_or_err = update_parameters_with_data( ref_path=ref_path, data=data, parameters=parameters, config=config ) - if isinstance(parameters_or_err, ParameterError): + if isinstance(parameters_or_err, ParameterError): # pragma: no cover next_round.append((name, data)) errors.append(parameters_or_err) continue diff --git a/openapi_python_client/parser/properties/any.py b/openapi_python_client/parser/properties/any.py index 65fcf40c4..e739c9c86 100644 --- a/openapi_python_client/parser/properties/any.py +++ b/openapi_python_client/parser/properties/any.py @@ -2,10 +2,11 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field -from ...utils import PythonIdentifier -from .protocol import PropertyProtocol, Value +from ...schema.untrusted_string import UntrustedString +from ...strings import PythonCode, PythonIdentifier +from .protocol import PropertyProtocol, Value, convert_example @define @@ -15,12 +16,12 @@ class AnyProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: UntrustedString | None, + example: Any, ) -> AnyProperty: return cls( name=name, @@ -39,13 +40,13 @@ def convert_value(cls, value: Any) -> Value | None: return value if isinstance(value, str): return StringProperty.convert_value(value) - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) - name: str + name: UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: UntrustedString | None + example: UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "Any" _json_type_string: ClassVar[str] = "Any" diff --git a/openapi_python_client/parser/properties/boolean.py b/openapi_python_client/parser/properties/boolean.py index 5fd4235d7..7c9b1941e 100644 --- a/openapi_python_client/parser/properties/boolean.py +++ b/openapi_python_client/parser/properties/boolean.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class BooleanProperty(PropertyProtocol): """Property for bool""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "bool" _json_type_string: ClassVar[str] = "bool" @@ -34,12 +34,12 @@ class BooleanProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> BooleanProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -54,14 +54,14 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if isinstance(value, Value) or value is None: return value if isinstance(value, str): if value.lower() == "true": - return Value(python_code="True", raw_value=value) + return Value(python_code=PythonCode("True"), raw_value=value) elif value.lower() == "false": - return Value(python_code="False", raw_value=value) + return Value(python_code=PythonCode("False"), raw_value=value) if isinstance(value, bool): - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) return PropertyError(f"Invalid boolean value: {value}") diff --git a/openapi_python_client/parser/properties/const.py b/openapi_python_client/parser/properties/const.py index 216941eb8..9480c3e05 100644 --- a/openapi_python_client/parser/properties/const.py +++ b/openapi_python_client/parser/properties/const.py @@ -4,7 +4,8 @@ from attr import define -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError from .protocol import PropertyProtocol, Value from .string import StringProperty @@ -14,12 +15,12 @@ class ConstProperty(PropertyProtocol): """A property representing a const value""" - name: str + name: oai.UntrustedString required: bool value: Value default: Value | None python_name: PythonIdentifier - description: str | None + description: oai.UntrustedString | None example: None template: ClassVar[str] = "const_property.py.jinja" @@ -29,10 +30,10 @@ def build( *, const: str | int | float | bool, default: Any, - name: str, + name: oai.UntrustedString, python_name: PythonIdentifier, required: bool, - description: str | None, + description: oai.UntrustedString | None, ) -> ConstProperty | PropertyError: """ Create a `ConstProperty` the right way. @@ -62,7 +63,7 @@ def build( prop.default = converted_default return prop - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: value = self._convert_value(value) if value is None: return value @@ -87,16 +88,16 @@ def _convert_value(value: Any) -> Value | None: return value if isinstance(value, str): return StringProperty.convert_value(value) - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) def get_type_string( self, no_optional: bool = False, json: bool = False, - ) -> str: - lit = f"Literal[{self.value.python_code}]" + ) -> PythonCode: + lit = PythonCode(f"Literal[{self.value.python_code.as_unembedded_code()}]") if not no_optional and not self.required: - return f"{lit} | Unset" + return PythonCode(f"{lit.as_unembedded_code()} | Unset") return lit def get_imports(self, *, prefix: str) -> set[str]: diff --git a/openapi_python_client/parser/properties/date.py b/openapi_python_client/parser/properties/date.py index 7bdae3d85..ab0c4b21a 100644 --- a/openapi_python_client/parser/properties/date.py +++ b/openapi_python_client/parser/properties/date.py @@ -3,23 +3,24 @@ import datetime from typing import Any, ClassVar -from attr import define +from attr import define, field -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class DateProperty(PropertyProtocol): """A property of type datetime.date""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "datetime.date" _json_type_string: ClassVar[str] = "str" @@ -28,12 +29,12 @@ class DateProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> DateProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -49,7 +50,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if isinstance(value, Value) or value is None: return value if isinstance(value, str): @@ -57,7 +58,7 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: datetime.date.fromisoformat(value) # make sure it's a valid value except ValueError as e: return PropertyError(f"Invalid date: {e}") - return Value(python_code=f"datetime.date.fromisoformat({value!r})", raw_value=value) + return Value(python_code=PythonCode(f"datetime.date.fromisoformat({value!r})"), raw_value=value) return PropertyError(f"Cannot convert {value} to a date") def get_imports(self, *, prefix: str) -> set[str]: diff --git a/openapi_python_client/parser/properties/datetime.py b/openapi_python_client/parser/properties/datetime.py index e6839dd86..c5b773979 100644 --- a/openapi_python_client/parser/properties/datetime.py +++ b/openapi_python_client/parser/properties/datetime.py @@ -3,11 +3,12 @@ import datetime from typing import Any, ClassVar -from attr import define +from attr import define, field -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define @@ -16,12 +17,12 @@ class DateTimeProperty(PropertyProtocol): A property of type datetime.datetime """ - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "datetime.datetime" _json_type_string: ClassVar[str] = "str" @@ -30,12 +31,12 @@ class DateTimeProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> DateTimeProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -51,7 +52,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value if isinstance(value, str): @@ -59,7 +60,7 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: datetime.datetime.fromisoformat(value) # make sure it's a valid value except ValueError as e: return PropertyError(f"Invalid datetime: {e}") - return Value(python_code=f"datetime.datetime.fromisoformat({value!r})", raw_value=value) + return Value(python_code=PythonCode(f"datetime.datetime.fromisoformat({value!r})"), raw_value=value) return PropertyError(f"Cannot convert {value} to a datetime") def get_imports(self, *, prefix: str) -> set[str]: diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index 9845da7f4..573d5f0d8 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -5,14 +5,14 @@ from typing import Any, ClassVar, cast from attr import evolve -from attrs import define +from attrs import define, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai from ...schema import DataType from ..errors import PropertyError from .none import NoneProperty -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Class, Schemas from .union import UnionProperty @@ -23,12 +23,12 @@ class EnumProperty(PropertyProtocol): """A property that should use an enum""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - description: str | None - example: str | None + python_name: strings.PythonIdentifier + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) values: dict[str, ValueType] class_info: Class value_type: type[ValueType] @@ -47,7 +47,7 @@ def build( # noqa: PLR0911 cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, @@ -83,7 +83,7 @@ def build( # noqa: PLR0911 name=name, required=required, default="None", - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=None, example=None, ), @@ -115,9 +115,9 @@ def build( # noqa: PLR0911 config=config, ) - class_name = data.title or name + class_name: str | oai.UntrustedString = data.title or name if parent_name: - class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" + class_name = f"{strings.pascal_case(parent_name)}{strings.pascal_case(class_name)}" class_info = Class.from_string(string=class_name, config=config) var_names = data.model_extra.get("x-enum-varnames", []) if data.model_extra else [] values = EnumProperty.values_from_list(value_list, class_info, var_names) @@ -139,7 +139,7 @@ def build( # noqa: PLR0911 values=values, value_type=value_type, default=None, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ) @@ -158,16 +158,19 @@ def convert_value(self, value: Any) -> Value | PropertyError | None: if isinstance(value, self.value_type): inverse_values = {v: k for k, v in self.values.items()} try: - return Value(python_code=f"{self.class_info.name}.{inverse_values[value]}", raw_value=value) + return Value( + python_code=strings.PythonCode(f"{self.class_info.name}.{inverse_values[value]}"), + raw_value=value, + ) except KeyError: return PropertyError(detail=f"Value {value} is not valid for enum {self.name}") return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}") - def get_base_type_string(self) -> str: - return self.class_info.name + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.class_info.name) - def get_base_json_type_string(self) -> str: - return self.value_type.__name__ + def get_base_json_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.value_type.__name__) def get_imports(self, *, prefix: str) -> set[str]: """ @@ -194,7 +197,7 @@ def values_from_list( if isinstance(value, int): if use_var_names: key = var_names[i] - sanitized_key = utils.snake_case(key).upper() + sanitized_key = strings.snake_case(key).upper() output[sanitized_key] = value elif value < 0: output[f"VALUE_NEGATIVE_{-value}"] = value @@ -212,6 +215,6 @@ def values_from_list( f"Duplicate key {key} in enum {class_info.module_name}.{class_info.name}; " f"consider setting literal_enums in your config" ) - sanitized_key = utils.snake_case(key).upper() - output[sanitized_key] = utils.remove_string_escapes(value) + sanitized_key = strings.snake_case(key).upper() + output[sanitized_key] = strings.in_double_quote_literal(value) return output diff --git a/openapi_python_client/parser/properties/file.py b/openapi_python_client/parser/properties/file.py index 90bbf6aec..eb1f6b0ab 100644 --- a/openapi_python_client/parser/properties/file.py +++ b/openapi_python_client/parser/properties/file.py @@ -2,23 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol +from .protocol import PropertyProtocol, convert_example @define class FileProperty(PropertyProtocol): """A property used for uploading files""" - name: str + name: oai.UntrustedString required: bool default: None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "File" # Return type of File.to_tuple() @@ -28,12 +29,12 @@ class FileProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> FileProperty | PropertyError: default_or_err = cls.convert_value(default) if isinstance(default_or_err, PropertyError): @@ -49,7 +50,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> None | PropertyError: + def convert_value(cls, value: Any) -> PropertyError | None: if value is not None: return PropertyError(detail="File properties cannot have a default value") return value diff --git a/openapi_python_client/parser/properties/float.py b/openapi_python_client/parser/properties/float.py index a785db6d4..0d5caa72a 100644 --- a/openapi_python_client/parser/properties/float.py +++ b/openapi_python_client/parser/properties/float.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class FloatProperty(PropertyProtocol): """A property of type float""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "float" _json_type_string: ClassVar[str] = "float" @@ -34,12 +34,12 @@ class FloatProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> FloatProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -55,17 +55,17 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if isinstance(value, Value) or value is None: return value if isinstance(value, str): try: parsed = float(value) - return Value(python_code=str(parsed), raw_value=value) + return Value(python_code=PythonCode(str(parsed)), raw_value=value) except ValueError: return PropertyError(f"Invalid float value: {value}") if isinstance(value, float): - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) if isinstance(value, int) and not isinstance(value, bool): - return Value(python_code=str(float(value)), raw_value=value) + return Value(python_code=PythonCode(str(float(value))), raw_value=value) return PropertyError(f"Cannot convert {value} to a float") diff --git a/openapi_python_client/parser/properties/int.py b/openapi_python_client/parser/properties/int.py index 1cd340fbd..579564f24 100644 --- a/openapi_python_client/parser/properties/int.py +++ b/openapi_python_client/parser/properties/int.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class IntProperty(PropertyProtocol): """A property of type int""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "int" _json_type_string: ClassVar[str] = "int" @@ -34,12 +34,12 @@ class IntProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> IntProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -55,7 +55,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value converted = value @@ -69,5 +69,5 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: if converted == as_int: converted = as_int if isinstance(converted, int) and not isinstance(converted, bool): - return Value(python_code=str(converted), raw_value=value) + return Value(python_code=PythonCode(str(converted)), raw_value=value) return PropertyError(f"Invalid int value: {value}") diff --git a/openapi_python_client/parser/properties/list_property.py b/openapi_python_client/parser/properties/list_property.py index 26ab0fdab..e331cddb4 100644 --- a/openapi_python_client/parser/properties/list_property.py +++ b/openapi_python_client/parser/properties/list_property.py @@ -2,12 +2,12 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import ReferencePath, Schemas @@ -15,12 +15,12 @@ class ListProperty(PropertyProtocol): """A property representing a list (array) of other properties""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - description: str | None - example: str | None + python_name: strings.PythonIdentifier + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) inner_property: PropertyProtocol template: ClassVar[str] = "list_property.py.jinja" @@ -29,13 +29,13 @@ def build( cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, config: Config, process_properties: bool, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[ListProperty | PropertyError, Schemas]: """ Build a ListProperty the right way, use this instead of the normal constructor. @@ -77,7 +77,7 @@ def build( inner_schema = oai.Schema(anyOf=items) inner_prop, schemas = property_from_data( - name=f"{name}_item", + name=oai.UntrustedString(f"{name.get_untrusted_value()}_item"), required=True, data=inner_schema, schemas=schemas, @@ -95,25 +95,25 @@ def build( required=required, default=None, inner_property=inner_prop, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), schemas, ) - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: return None # pragma: no cover - def get_base_type_string(self) -> str: - return f"list[{self.inner_property.get_type_string()}]" + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(f"list[{self.inner_property.get_type_string().as_unembedded_code()}]") - def get_base_json_type_string(self) -> str: - return f"list[{self.inner_property.get_type_string(json=True)}]" + def get_base_json_type_string(self) -> strings.PythonCode: + return strings.PythonCode(f"list[{self.inner_property.get_type_string(json=True).as_unembedded_code()}]") - def get_instance_type_string(self) -> str: - """Get a string representation of runtime type that should be used for `isinstance` checks""" - return "list" + def get_instance_type_string(self) -> strings.PythonCode: + """Get a code representation of runtime type that should be used for `isinstance` checks""" + return strings.PythonCode("list") def get_imports(self, *, prefix: str) -> set[str]: """ @@ -137,9 +137,9 @@ def get_type_string( self, no_optional: bool = False, json: bool = False, - ) -> str: + ) -> strings.PythonCode: """ - Get a string representation of type that should be used when declaring this property + Get a Python code representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) @@ -152,4 +152,4 @@ def get_type_string( if no_optional or self.required: return type_string - return f"{type_string} | Unset" + return strings.PythonCode(f"{type_string.as_unembedded_code()} | Unset") diff --git a/openapi_python_client/parser/properties/literal_enum_property.py b/openapi_python_client/parser/properties/literal_enum_property.py index f0e0306a2..0388fe04e 100644 --- a/openapi_python_client/parser/properties/literal_enum_property.py +++ b/openapi_python_client/parser/properties/literal_enum_property.py @@ -5,14 +5,14 @@ from typing import Any, ClassVar, cast from attr import evolve -from attrs import define +from attrs import define, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai from ...schema import DataType from ..errors import PropertyError from .none import NoneProperty -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Class, Schemas from .union import UnionProperty @@ -23,12 +23,12 @@ class LiteralEnumProperty(PropertyProtocol): """A property that should use a literal enum""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - description: str | None - example: str | None + python_name: strings.PythonIdentifier + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) values: set[ValueType] class_info: Class value_type: type[ValueType] @@ -47,7 +47,7 @@ def build( # noqa: PLR0911 cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, @@ -82,7 +82,7 @@ def build( # noqa: PLR0911 name=name, required=required, default="None", - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=None, example=None, ), @@ -114,9 +114,9 @@ def build( # noqa: PLR0911 config=config, ) - class_name = data.title or name + class_name: str | oai.UntrustedString = data.title or name if parent_name: - class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" + class_name = f"{strings.pascal_case(parent_name)}{strings.pascal_case(class_name)}" class_info = Class.from_string(string=class_name, config=config) values: set[str | int] = set(value_list) @@ -137,7 +137,7 @@ def build( # noqa: PLR0911 values=values, value_type=value_type, default=None, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ) @@ -155,19 +155,19 @@ def convert_value(self, value: Any) -> Value | PropertyError | None: return value if isinstance(value, self.value_type): if value in self.values: - return Value(python_code=repr(value), raw_value=value) + return Value(python_code=strings.PythonCode(repr(value)), raw_value=value) else: return PropertyError(detail=f"Value {value} is not valid for enum {self.name}") return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}") - def get_base_type_string(self) -> str: - return self.class_info.name + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.class_info.name) - def get_base_json_type_string(self) -> str: - return self.value_type.__name__ + def get_base_json_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.value_type.__name__) - def get_instance_type_string(self) -> str: - return self.value_type.__name__ + def get_instance_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.value_type.__name__) def get_imports(self, *, prefix: str) -> set[str]: """ @@ -186,4 +186,4 @@ def get_imports(self, *, prefix: str) -> set[str]: return imports def get_class_name_snake_case(self) -> str: - return utils.snake_case(self.class_info.name) + return strings.snake_case(self.class_info.name) diff --git a/openapi_python_client/parser/properties/merge_properties.py b/openapi_python_client/parser/properties/merge_properties.py index 57b4bccbe..2ccf7f9e4 100644 --- a/openapi_python_client/parser/properties/merge_properties.py +++ b/openapi_python_client/parser/properties/merge_properties.py @@ -67,11 +67,12 @@ def merge_properties(prop1: Property, prop2: Property) -> Property | PropertyErr return merged return PropertyError( - detail=f"{prop1.get_type_string(no_optional=True)} can't be merged with {prop2.get_type_string(no_optional=True)}" + detail=f"{prop1.get_type_string(no_optional=True).as_unembedded_code()} can't be merged with " + f"{prop2.get_type_string(no_optional=True).as_unembedded_code()}" ) -def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | PropertyError: +def _merge_same_type(prop1: Property, prop2: Property) -> Property | PropertyError | None: if type(prop1) is not type(prop2): return None @@ -90,7 +91,7 @@ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | Prop return _merge_common_attributes(prop1, prop2) -def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | None | PropertyError: +def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | PropertyError | None: """Merge a string that has no format with a string that has a format""" # Here we need to use the DateProperty/DateTimeProperty/FileProperty as the base so that we preserve # its class, but keep the correct override order for merging the attributes. @@ -103,7 +104,7 @@ def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | No return None -def _merge_numeric(prop1: Property, prop2: Property) -> IntProperty | None | PropertyError: +def _merge_numeric(prop1: Property, prop2: Property) -> IntProperty | PropertyError | None: """Merge IntProperty with FloatProperty""" if isinstance(prop1, IntProperty) and isinstance(prop2, IntProperty | FloatProperty): return _merge_common_attributes(prop1, prop2) @@ -136,7 +137,8 @@ def _merge_with_enum(prop1: PropertyProtocol, prop2: PropertyProtocol) -> EnumPr ): return _merge_common_attributes(enum_prop, prop1, prop2) return PropertyError( - detail=f"can't combine enum of type {enum_prop.value_type} with {non_enum_prop.get_type_string(no_optional=True)}" + detail=f"can't combine enum of type {enum_prop.value_type} with " + f"{non_enum_prop.get_type_string(no_optional=True).as_unembedded_code()}" ) @@ -162,7 +164,8 @@ def _merge_with_literal_enum(prop1: PropertyProtocol, prop2: PropertyProtocol) - ): return _merge_common_attributes(enum_prop, prop1, prop2) return PropertyError( - detail=f"can't combine literal enum of type {enum_prop.value_type} with {non_enum_prop.get_type_string(no_optional=True)}" + detail=f"can't combine literal enum of type {enum_prop.value_type} with " + f"{non_enum_prop.get_type_string(no_optional=True).as_unembedded_code()}" ) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 636b71a34..7e3b545e0 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -3,14 +3,14 @@ from itertools import chain from typing import Any, ClassVar, NamedTuple -from attrs import define, evolve +from attrs import define, evolve, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonIdentifier from ..errors import ParseError, PropertyError from .any import AnyProperty -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Class, ReferencePath, Schemas, parse_reference_path @@ -18,15 +18,15 @@ class ModelProperty(PropertyProtocol): """A property which refers to another Schema""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - example: str | None + python_name: strings.PythonIdentifier + example: oai.UntrustedString | None = field(converter=convert_example) class_info: Class data: oai.Schema - description: str - roots: set[ReferencePath | utils.ClassName] + description: oai.UntrustedString | None + roots: set[ReferencePath | strings.ClassName] required_properties: list[Property] | None optional_properties: list[Property] | None relative_imports: set[str] | None @@ -43,13 +43,13 @@ def build( cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, schemas: Schemas, required: bool, parent_name: str | None, config: Config, process_properties: bool, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[ModelProperty | PropertyError, Schemas]: """ A single ModelProperty from its OAI data @@ -65,12 +65,13 @@ def build( roots: Set of strings that identify schema objects on which the new ModelProperty will depend process_properties: Determines whether the new ModelProperty will be initialized with property data """ + class_string: str | oai.UntrustedString if not config.use_path_prefixes_for_title_model_names and data.title: class_string = data.title else: title = data.title or name if parent_name: - class_string = f"{utils.pascal_case(parent_name)}{utils.pascal_case(title)}" + class_string = f"{strings.pascal_case(parent_name)}{strings.pascal_case(title)}" else: class_string = title class_info = Class.from_string(string=class_string, config=config) @@ -92,7 +93,7 @@ def build( relative_imports = property_data.relative_imports lazy_imports = property_data.lazy_imports for root in roots: - if isinstance(root, utils.ClassName): + if isinstance(root, strings.ClassName): continue schemas.add_dependencies(root, {class_info.name}) @@ -105,11 +106,11 @@ def build( relative_imports=relative_imports, lazy_imports=lazy_imports, additional_properties=additional_properties, - description=data.description or "", + description=data.description, default=None, required=required, name=name, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), example=data.example, ) if class_info.name in schemas.classes_by_name: @@ -126,7 +127,7 @@ def build( return prop, schemas @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is not None: return PropertyError(detail="ModelProperty cannot have a default value") # pragma: no cover return None @@ -140,8 +141,8 @@ def self_import(self) -> str: """Constructs a self import statement from this ModelProperty's attributes""" return f"models.{self.class_info.module_name} import {self.class_info.name}" - def get_base_type_string(self) -> str: - return self.class_info.name + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.class_info.name) def get_imports(self, *, prefix: str) -> set[str]: """ @@ -188,9 +189,9 @@ def get_type_string( self, no_optional: bool = False, json: bool = False, - ) -> str: + ) -> strings.PythonCode: """ - Get a string representation of type that should be used when declaring this property + Get a Python code representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) @@ -203,7 +204,7 @@ def get_type_string( if no_optional or self.required: return type_string - return f"{type_string} | Unset" + return strings.PythonCode(f"{type_string.as_unembedded_code()} | Unset") from .property import Property # noqa: E402 @@ -232,14 +233,14 @@ def _process_properties( # noqa: PLR0912, PLR0911 *, data: oai.Schema, schemas: Schemas, - class_name: utils.ClassName, + class_name: strings.ClassName, config: Config, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> _PropertyData | PropertyError: from . import property_from_data # noqa: PLC0415 from .merge_properties import merge_properties # noqa: PLC0415 - properties: dict[str, Property] = {} + properties: dict[oai.UntrustedString, Property] = {} relative_imports: set[str] = set() lazy_imports: set[str] = set() required_set = set(data.required or []) @@ -265,7 +266,7 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: properties[merged_prop.name] = merged_prop return None - unprocessed_props: list[tuple[str, oai.Reference | oai.Schema]] = ( + unprocessed_props: list[tuple[oai.UntrustedString, oai.Reference | oai.Schema]] = ( list(data.properties.items()) if data.properties else [] ) for sub_prop in data.allOf: @@ -275,7 +276,7 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: return PropertyError(detail=ref_path.detail, data=sub_prop) sub_model = schemas.classes_by_reference.get(ref_path) if sub_model is None: - return PropertyError(f"Reference {sub_prop.ref} not found") + return PropertyError(f"Reference {sub_prop.ref.get_untrusted_value()} not found") if not isinstance(sub_model, ModelProperty): return PropertyError("Cannot take allOf a non-object") # Properties of allOf references first should be processed first @@ -335,10 +336,10 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: ANY_ADDITIONAL_PROPERTY = AnyProperty.build( - name="additional", + name=oai.UntrustedString("additional"), required=True, default=None, - description="", + description=None, python_name=PythonIdentifier(value="additional", prefix=""), example=None, ) @@ -348,10 +349,10 @@ def _get_additional_properties( *, schema_additional: None | (bool | (oai.Reference | oai.Schema)), schemas: Schemas, - class_name: utils.ClassName, + class_name: strings.ClassName, config: Config, - roots: set[ReferencePath | utils.ClassName], -) -> tuple[Property | None | PropertyError, Schemas]: + roots: set[ReferencePath | strings.ClassName], +) -> tuple[Property | PropertyError | None, Schemas]: from . import property_from_data # noqa: PLC0415 if schema_additional is None: @@ -367,7 +368,7 @@ def _get_additional_properties( return ANY_ADDITIONAL_PROPERTY, schemas additional_properties, schemas = property_from_data( - name="AdditionalProperty", + name=oai.UntrustedString("AdditionalProperty"), required=True, # in the sense that if present in the dict will not be None data=schema_additional, schemas=schemas, @@ -384,7 +385,7 @@ def _process_property_data( schemas: Schemas, class_info: Class, config: Config, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[tuple[_PropertyData, Property | None] | PropertyError, Schemas]: property_data = _process_properties( data=data, schemas=schemas, class_name=class_info.name, config=config, roots=roots diff --git a/openapi_python_client/parser/properties/none.py b/openapi_python_client/parser/properties/none.py index 9c473693d..54bc69a29 100644 --- a/openapi_python_client/parser/properties/none.py +++ b/openapi_python_client/parser/properties/none.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class NoneProperty(PropertyProtocol): """A property that can only be None""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { oai.ParameterLocation.QUERY, @@ -32,12 +32,12 @@ class NoneProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> NoneProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -52,10 +52,10 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value if isinstance(value, str): if value == "None": - return Value(python_code=value, raw_value=value) + return Value(python_code=PythonCode(value), raw_value=value) return PropertyError(f"Value {value} is not valid, only None is allowed") diff --git a/openapi_python_client/parser/properties/protocol.py b/openapi_python_client/parser/properties/protocol.py index 138fa7002..d4fabb014 100644 --- a/openapi_python_client/parser/properties/protocol.py +++ b/openapi_python_client/parser/properties/protocol.py @@ -1,20 +1,29 @@ from __future__ import annotations -__all__ = ["PropertyProtocol", "Value"] +__all__ = ["PropertyProtocol", "Value", "convert_example"] from abc import abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar +from typing import Any, ClassVar, Protocol, TypeVar from ... import Config from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier, safe_for_docstring from ..errors import ParseError, PropertyError -if TYPE_CHECKING: # pragma: no cover - from .model_property import ModelProperty -else: - ModelProperty = "ModelProperty" + +def convert_example(example: Any) -> oai.UntrustedString | None: + """ + Convert a raw OpenAPI `example` (which can be anything) into an `UntrustedString` (or None). + + Examples only ever appear in docstrings, so non-string examples are stringified. Wrapping ensures a + template which uses an example without escaping renders a safe-but-broken value instead of an exploit. + """ + if example is None or isinstance(example, oai.UntrustedString): + return example + if isinstance(example, str): + return oai.UntrustedString(example) + return oai.UntrustedString(str(example)) @dataclass @@ -24,7 +33,7 @@ class Value: (with string escaping, for example). We still keep the `raw_value` around for merging `allOf`. """ - python_code: str + python_code: PythonCode raw_value: Any @@ -45,7 +54,7 @@ class PropertyProtocol(Protocol): ValidationError: Raised when the default value fails to be converted to the expected type """ - name: str + name: oai.UntrustedString required: bool _type_string: ClassVar[str] = "" _json_type_string: ClassVar[str] = "" # Type of the property after JSON serialization @@ -56,26 +65,26 @@ class PropertyProtocol(Protocol): } default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None template: ClassVar[str] = "any_property.py.jinja" json_is_dict: ClassVar[bool] = False @abstractmethod - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: """Convert a string value to a Value object""" raise NotImplementedError() # pragma: no cover def validate_location(self, location: oai.ParameterLocation) -> ParseError | None: """Returns an error if this type of property is not allowed in the given location""" if location not in self._allowed_locations: - return ParseError(detail=f"{self.get_type_string()} is not allowed in {location}") + return ParseError(detail=f"{self.get_type_string().as_unembedded_code()} is not allowed in {location}") if location == oai.ParameterLocation.PATH and not self.required: return ParseError(detail="Path parameter must be required") return None - def set_python_name(self, new_name: str, config: Config, skip_snake_case: bool = False) -> None: + def set_python_name(self, new_name: oai.UntrustedString, config: Config, skip_snake_case: bool = False) -> None: """Mutates this Property to set a new python_name. Required to mutate due to how Properties are stored and the difficulty of updating them in-dict. @@ -88,21 +97,21 @@ def set_python_name(self, new_name: str, config: Config, skip_snake_case: bool = PythonIdentifier(value=new_name, prefix=config.field_prefix, skip_snake_case=skip_snake_case), ) - def get_base_type_string(self) -> str: - """Get the string describing the Python type of this property. Base types no require quoting.""" - return self._type_string + def get_base_type_string(self) -> PythonCode: + """Get the code describing the Python type of this property. Base types no require quoting.""" + return PythonCode(self._type_string) - def get_base_json_type_string(self) -> str: - """Get the string describing the JSON type of this property. Base types no require quoting.""" - return self._json_type_string + def get_base_json_type_string(self) -> PythonCode: + """Get the code describing the JSON type of this property. Base types no require quoting.""" + return PythonCode(self._json_type_string) def get_type_string( self, no_optional: bool = False, json: bool = False, - ) -> str: + ) -> PythonCode: """ - Get a string representation of type that should be used when declaring this property + Get a Python code representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) @@ -115,10 +124,10 @@ def get_type_string( if no_optional or self.required: return type_string - return f"{type_string} | Unset" + return PythonCode(f"{type_string.as_unembedded_code()} | Unset") - def get_instance_type_string(self) -> str: - """Get a string representation of runtime type that should be used for `isinstance` checks""" + def get_instance_type_string(self) -> PythonCode: + """Get a Python code representation of runtime type that should be used for `isinstance` checks""" return self.get_type_string(no_optional=True) # noinspection PyUnusedLocal @@ -144,25 +153,26 @@ def get_lazy_imports(self, *, prefix: str) -> set[str]: """ return set() - def to_string(self) -> str: + def to_string(self) -> PythonCode: """How this should be declared in a dataclass""" default: str | None if self.default is not None: - default = self.default.python_code + default = self.default.python_code.as_unembedded_code() elif not self.required: default = "UNSET" else: default = None + declaration = f"{self.python_name}: {self.get_type_string().as_unembedded_code()}" if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {default}" - return f"{self.python_name}: {self.get_type_string()}" + declaration += f" = {default}" + return PythonCode(declaration) def to_docstring(self) -> str: """Returns property docstring""" - doc = f"{self.python_name} ({self.get_type_string()}): {self.description or ''}" + doc = f"{self.python_name} ({safe_for_docstring(self.get_type_string())}): {safe_for_docstring(self.description or '')}" if self.default: - doc += f" Default: {self.default.python_code}." + doc += f" Default: {safe_for_docstring(self.default.python_code)}." if self.example: - doc += f" Example: {self.example}." + doc += f" Example: {safe_for_docstring(self.example)}." return doc diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index dfa7b0f20..49e278437 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -10,7 +10,7 @@ "update_schemas_with_data", ] -from typing import TYPE_CHECKING, NewType, cast +from typing import TYPE_CHECKING, NewType from urllib.parse import urlparse from attrs import define, evolve, field @@ -18,7 +18,9 @@ from ... import Config from ... import schema as oai from ...schema.openapi_schema_pydantic import Parameter -from ...utils import ClassName, PythonIdentifier +from ...schema.ref import Ref +from ...schema.untrusted_string import UntrustedString +from ...strings import ClassName, PythonIdentifier from ..errors import ParameterError, ParseError, PropertyError if TYPE_CHECKING: # pragma: no cover @@ -29,10 +31,10 @@ Property = "Property" -ReferencePath = NewType("ReferencePath", str) +ReferencePath = NewType("ReferencePath", UntrustedString) -def parse_reference_path(ref_path_raw: str) -> ReferencePath | ParseError: +def parse_reference_path(ref_path_raw: Ref) -> ReferencePath | ParseError: """ Takes a raw string provided in a `$ref` and turns it into a validated `_ReferencePath` or a `ParseError` if validation fails. @@ -40,17 +42,17 @@ def parse_reference_path(ref_path_raw: str) -> ReferencePath | ParseError: See Also: - https://swagger.io/docs/specification/using-ref/ """ - parsed = urlparse(ref_path_raw) + parsed = urlparse(ref_path_raw.get_untrusted_value()) if parsed.scheme or parsed.path: - return ParseError(detail=f"Remote references such as {ref_path_raw} are not supported yet.") - return cast(ReferencePath, parsed.fragment) + return ParseError(detail=f"Remote references such as {parsed} are not supported yet.") + return ReferencePath(UntrustedString(parsed.fragment)) -def get_reference_simple_name(ref_path: str) -> str: +def get_reference_simple_name(ref_path: Ref | UntrustedString) -> UntrustedString: """ Takes a path like `/components/schemas/NameOfThing` and returns a string like `NameOfThing`. """ - return ref_path.rsplit("/", maxsplit=1)[-1] + return UntrustedString(ref_path.get_untrusted_value().rsplit("/", maxsplit=1)[-1]) @define @@ -61,10 +63,10 @@ class Class: module_name: PythonIdentifier @staticmethod - def from_string(*, string: str, config: Config) -> "Class": + def from_string(*, string: str | UntrustedString, config: Config) -> "Class": """Get a Class from an arbitrary string""" - class_name = get_reference_simple_name(string) # Get rid of ref path stuff - class_name = ClassName(class_name, config.field_prefix) + untrusted_class_name = get_reference_simple_name(Ref(string)) # Get rid of ref path stuff + class_name = ClassName(untrusted_class_name, config.field_prefix) override = config.class_overrides.get(class_name) if override is not None and override.class_name is not None: @@ -134,12 +136,15 @@ def update_schemas_with_data( ) if isinstance(prop, PropertyError): - prop.detail = f"{prop.header}: {prop.detail}" - prop.header = f"Unable to parse schema {ref_path}" - if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith(ref_path): # pragma: nocover - prop.detail += ( + detail = f"{prop.header}: {prop.detail}" if prop.detail is not None else prop.header + prop.header = f"Unable to parse schema {ref_path.get_untrusted_value()}" + if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith( + ref_path.get_untrusted_value() + ): # pragma: nocover + detail += ( "\n\nRecursive and circular references are not supported directly in an array schema's 'items' section" ) + prop.detail = detail return prop schemas = evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference}) @@ -157,7 +162,7 @@ class Parameters: def parameter_from_data( *, - name: str, + name: UntrustedString, data: oai.Reference | oai.Parameter, parameters: Parameters, config: Config, @@ -171,10 +176,10 @@ def parameter_from_data( return ParameterError("Parameter has no schema"), parameters new_param = Parameter( - name=name, + name=name.get_untrusted_value(), required=data.required, explode=data.explode, - style=data.style, + style=data.style.get_untrusted_value() if data.style is not None else None, param_schema=data.param_schema, param_in=data.param_in, ) @@ -203,10 +208,12 @@ def update_parameters_with_data( """ param, parameters = parameter_from_data(data=data, name=data.name, parameters=parameters, config=config) - if isinstance(param, ParameterError): + if isinstance(param, ParameterError): # pragma: no cover param.detail = f"{param.header}: {param.detail}" - param.header = f"Unable to parse parameter {ref_path}" - if isinstance(param.data, oai.Reference) and param.data.ref.endswith(ref_path): # pragma: nocover + param.header = f"Unable to parse parameter {ref_path.get_untrusted_value()}" + if isinstance(param.data, oai.Reference) and param.data.ref.endswith( + ref_path.get_untrusted_value() + ): # pragma: nocover param.detail += ( "\n\nRecursive and circular references are not supported. " "See https://github.com/openapi-generators/openapi-python-client/issues/466" @@ -245,5 +252,5 @@ def parameter_from_reference( _resolved_parameter_class = parameters.classes_by_reference.get(ref_path, None) if _resolved_parameter_class is None: - return ParameterError(detail=f"Reference `{ref_path}` not found.") + return ParameterError(detail=f"Reference `{ref_path.get_untrusted_value()}` not found.") return _resolved_parameter_class diff --git a/openapi_python_client/parser/properties/string.py b/openapi_python_client/parser/properties/string.py index e40c1eee6..618984114 100644 --- a/openapi_python_client/parser/properties/string.py +++ b/openapi_python_client/parser/properties/string.py @@ -2,25 +2,24 @@ from typing import Any, ClassVar, overload -from attr import define +from attr import define, field from ... import schema as oai -from ... import utils -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier, remove_string_escapes from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class StringProperty(PropertyProtocol): """A property of type str""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "str" _json_type_string: ClassVar[str] = "str" _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { @@ -33,12 +32,12 @@ class StringProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> StringProperty | PropertyError: checked_default = cls.convert_value(default) return cls( @@ -65,4 +64,4 @@ def convert_value(cls, value: Any) -> Value | None: return value if not isinstance(value, str): value = str(value) - return Value(python_code=repr(utils.remove_string_escapes(value)), raw_value=value) + return Value(python_code=PythonCode(repr(remove_string_escapes(value))), raw_value=value) diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py index 3091c793b..788e803a6 100644 --- a/openapi_python_client/parser/properties/union.py +++ b/openapi_python_client/parser/properties/union.py @@ -4,13 +4,13 @@ from itertools import chain from typing import Any, ClassVar, cast -from attr import define, evolve +from attr import define, evolve, field -from ... import Config +from ... import Config, strings from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonIdentifier from ..errors import ParseError, PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Schemas @@ -18,12 +18,12 @@ class UnionProperty(PropertyProtocol): """A property representing a Union (anyOf) of other properties""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) inner_properties: list[PropertyProtocol] template: ClassVar[str] = "union_property.py.jinja" @@ -32,7 +32,7 @@ def build( cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, @@ -70,12 +70,12 @@ def build( and sub_prop_data.title is not None and sub_prop_data.title != data.title ): - subscript = sub_prop_data.title + subscript = sub_prop_data.title.get_untrusted_value() else: subscript = f"type_{i}" sub_prop, schemas = property_from_data( - name=f"{name}_{subscript}", + name=oai.UntrustedString(f"{name.get_untrusted_value()}_{subscript}"), required=True, data=sub_prop_data, schemas=schemas, @@ -120,7 +120,7 @@ def flatten_union_properties(possibly_nested: list[PropertyProtocol]) -> Iterato prop = evolve(prop, default=default_or_error) return prop, schemas - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return None value_or_error: Value | PropertyError | None = PropertyError( @@ -132,7 +132,7 @@ def convert_value(self, value: Any) -> Value | None | PropertyError: return value_or_error return value_or_error - def _get_inner_type_strings(self, json: bool) -> set[str]: + def _get_inner_type_strings(self, json: bool) -> set[strings.PythonCode]: return { p.get_type_string( no_optional=True, @@ -142,18 +142,19 @@ def _get_inner_type_strings(self, json: bool) -> set[str]: } @staticmethod - def _get_type_string_from_inner_type_strings(inner_types: set[str]) -> str: + def _get_type_string_from_inner_type_strings(inner_types: set[strings.PythonCode]) -> strings.PythonCode: if len(inner_types) == 1: return inner_types.pop() - return " | ".join(sorted(inner_types, key=lambda x: x.lower())) + joined = " | ".join(sorted((t.as_unembedded_code() for t in inner_types), key=str.lower)) + return strings.PythonCode(joined) - def get_base_type_string(self) -> str: + def get_base_type_string(self) -> strings.PythonCode: return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=False)) - def get_base_json_type_string(self) -> str: + def get_base_json_type_string(self) -> strings.PythonCode: return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=True)) - def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool) -> set[str]: + def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool) -> set[strings.PythonCode]: """ Get the set of all the types that should appear within the `Union` representing this property. @@ -164,22 +165,22 @@ def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool) -> json: If True, this returns the JSON types, not the Python types, of this property. Returns: - A set of strings containing the types that should appear within `Union`. + A set of code snippets containing the types that should appear within `Union`. """ type_strings = self._get_inner_type_strings(json=json) if no_optional: return type_strings if not self.required: - type_strings.add("Unset") + type_strings.add(strings.PythonCode("Unset")) return type_strings def get_type_string( self, no_optional: bool = False, json: bool = False, - ) -> str: + ) -> strings.PythonCode: """ - Get a string representation of type that should be used when declaring this property. + Get a Python code representation of type that should be used when declaring this property. This implementation differs slightly from `Property.get_type_string` in order to collapse nested union types. """ @@ -212,5 +213,6 @@ def validate_location(self, location: oai.ParameterLocation) -> ParseError | Non for inner_prop in self.inner_properties: if evolve(cast(Property, inner_prop), required=self.required).validate_location(location) is not None: - return ParseError(detail=f"{self.get_type_string()} is not allowed in {location}") + type_string = self.get_type_string().as_unembedded_code() + return ParseError(detail=f"{type_string} is not allowed in {location}") return None diff --git a/openapi_python_client/parser/properties/uuid.py b/openapi_python_client/parser/properties/uuid.py index 86d7d6a0a..c813fe200 100644 --- a/openapi_python_client/parser/properties/uuid.py +++ b/openapi_python_client/parser/properties/uuid.py @@ -3,24 +3,24 @@ from typing import Any, ClassVar from uuid import UUID -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class UuidProperty(PropertyProtocol): """A property of type uuid.UUID""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "UUID" _json_type_string: ClassVar[str] = "str" @@ -35,12 +35,12 @@ class UuidProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> UuidProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -56,7 +56,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value if isinstance(value, str): @@ -64,7 +64,7 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: UUID(value) except ValueError: return PropertyError(f"Invalid UUID value: {value}") - return Value(python_code=f"UUID('{value}')", raw_value=value) + return Value(python_code=PythonCode(f"UUID('{value}')"), raw_value=value) return PropertyError(f"Invalid UUID value: {value}") def get_imports(self, *, prefix: str) -> set[str]: diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index f05fbce75..ef1c212a8 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -5,12 +5,13 @@ from attrs import define -from openapi_python_client import utils +from openapi_python_client import strings from openapi_python_client.parser.properties.schemas import get_reference_simple_name, parse_reference_path from .. import Config from .. import schema as oai -from ..utils import PythonIdentifier +from ..schema.untrusted_string import UntrustedString +from ..strings import PythonIdentifier from .errors import ParseError, PropertyError from .properties import AnyProperty, Property, Schemas, property_from_data @@ -60,22 +61,23 @@ def __init__(self, *, pattern: str, code_range: tuple[int, int] | None): self.range = code_range @staticmethod - def parse(pattern: str) -> "HTTPStatusPattern | ParseError": + def parse(pattern: UntrustedString) -> "HTTPStatusPattern | ParseError": """Parse a status code pattern such as 2XX or 404""" - if pattern == "default": - return HTTPStatusPattern(pattern=pattern, code_range=None) + untrusted = pattern.get_untrusted_value() + if untrusted == "default": + return HTTPStatusPattern(pattern=untrusted, code_range=None) - if pattern.endswith("XX") and pattern[0].isdigit(): - first_digit = int(pattern[0]) - return HTTPStatusPattern(pattern=pattern, code_range=(first_digit * 100, first_digit * 100 + 99)) + if len(untrusted) == 3 and untrusted.endswith("XX") and untrusted[0].isdigit(): + first_digit = int(untrusted[0]) + return HTTPStatusPattern(pattern=untrusted, code_range=(first_digit * 100, first_digit * 100 + 99)) try: - code = int(pattern) - return HTTPStatusPattern(pattern=pattern, code_range=(code, code)) + code = int(untrusted) + return HTTPStatusPattern(pattern=untrusted, code_range=(code, code)) except ValueError: return ParseError( detail=( - f"Invalid response status code pattern: {pattern}, response will be omitted from generated client" + f"Invalid response status code pattern: {untrusted}, response will be omitted from generated client" ) ) @@ -129,7 +131,7 @@ def __lt__(self, other: "Response") -> bool: def _source_by_content_type(content_type: str, config: Config) -> _ResponseSource | None: - parsed_content_type = utils.get_content_type(content_type, config) + parsed_content_type = strings.get_content_type(content_type, config) if parsed_content_type is None: return None @@ -150,7 +152,7 @@ def _source_by_content_type(content_type: str, config: Config) -> _ResponseSourc def empty_response( *, status_code: HTTPStatusPattern, - response_name: str, + response_name: UntrustedString, config: Config, data: oai.Response | oai.Reference, ) -> Response: @@ -175,23 +177,25 @@ def response_from_data( # noqa: PLR0911 status_code: HTTPStatusPattern, data: oai.Response | oai.Reference, schemas: Schemas, - responses: dict[str, oai.Response | oai.Reference], + responses: dict[UntrustedString, oai.Response | oai.Reference], parent_name: str, config: Config, ) -> tuple[Response | ParseError, Schemas]: """Generate a Response from the OpenAPI dictionary representation of it""" - response_name = f"response_{status_code.pattern}" + response_name = UntrustedString(f"response_{status_code.pattern}") if isinstance(data, oai.Reference): ref_path = parse_reference_path(data.ref) - if isinstance(ref_path, ParseError): + if isinstance(ref_path, ParseError): # pragma: no cover return ref_path, schemas - if not ref_path.startswith("/components/responses/"): - return ParseError(data=data, detail=f"$ref to {data.ref} not allowed in responses"), schemas + if not ref_path.startswith("/components/responses/"): # pragma: no cover + return ParseError( + data=data, detail=f"$ref to {data.ref.get_untrusted_value()} not allowed in responses" + ), schemas resp_data = responses.get(get_reference_simple_name(ref_path), None) - if not resp_data: - return ParseError(data=data, detail=f"Could not find reference: {data.ref}"), schemas - if not isinstance(resp_data, oai.Response): + if not resp_data: # pragma: no cover + return ParseError(data=data, detail=f"Could not find reference: {data.ref.get_untrusted_value()}"), schemas + if not isinstance(resp_data, oai.Response): # pragma: no cover return ParseError(data=data, detail="Top-level $ref inside components/responses is not supported"), schemas data = resp_data @@ -208,13 +212,16 @@ def response_from_data( # noqa: PLR0911 ) for content_type, media_type in content.items(): - source = _source_by_content_type(content_type, config) + source = _source_by_content_type(content_type.get_untrusted_value(), config) if source is not None: schema_data = media_type.media_type_schema break else: return ( - ParseError(data=data, detail=f"Unsupported content_type {content}"), + ParseError( + data=data, + detail=f"Unsupported content_type {', '.join(key.get_untrusted_value() for key in content)}", + ), schemas, ) diff --git a/openapi_python_client/schema/__init__.py b/openapi_python_client/schema/__init__.py index 21a90f5fb..41ed92cf2 100644 --- a/openapi_python_client/schema/__init__.py +++ b/openapi_python_client/schema/__init__.py @@ -4,7 +4,6 @@ "OpenAPI", "Operation", "Parameter", - "Parameter", "ParameterLocation", "PathItem", "Reference", @@ -12,6 +11,7 @@ "Response", "Responses", "Schema", + "UntrustedString", ] @@ -29,3 +29,4 @@ Schema, ) from .parameter_location import ParameterLocation +from .untrusted_string import UntrustedString diff --git a/openapi_python_client/schema/openapi_schema_pydantic/callback.py b/openapi_python_client/schema/openapi_schema_pydantic/callback.py index f4593cc8d..ddf6a3434 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/callback.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/callback.py @@ -1,11 +1,13 @@ from typing import TYPE_CHECKING +from ..untrusted_string import UntrustedString + if TYPE_CHECKING: # pragma: no cover from .path_item import PathItem else: PathItem = "PathItem" -Callback = dict[str, PathItem] +Callback = dict[UntrustedString, PathItem] """ A map of possible out-of band callbacks related to the parent operation. Each value in the map is a [Path Item Object](#pathItemObject) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/components.py b/openapi_python_client/schema/openapi_schema_pydantic/components.py index 3023714c9..8423247d7 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/components.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/components.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .callback import Callback from .example import Example from .header import Header @@ -23,15 +24,15 @@ class Components(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject """ - schemas: dict[str, ReferenceOr[Schema]] | None = None - responses: dict[str, ReferenceOr[Response]] | None = None - parameters: dict[str, ReferenceOr[Parameter]] | None = None - examples: dict[str, ReferenceOr[Example]] | None = None - requestBodies: dict[str, ReferenceOr[RequestBody]] | None = None - headers: dict[str, ReferenceOr[Header]] | None = None - securitySchemes: dict[str, ReferenceOr[SecurityScheme]] | None = None - links: dict[str, ReferenceOr[Link]] | None = None - callbacks: dict[str, ReferenceOr[Callback]] | None = None + schemas: dict[UntrustedString, ReferenceOr[Schema]] | None = None + responses: dict[UntrustedString, ReferenceOr[Response]] | None = None + parameters: dict[UntrustedString, ReferenceOr[Parameter]] | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + requestBodies: dict[UntrustedString, ReferenceOr[RequestBody]] | None = None + headers: dict[UntrustedString, ReferenceOr[Header]] | None = None + securitySchemes: dict[UntrustedString, ReferenceOr[SecurityScheme]] | None = None + links: dict[UntrustedString, ReferenceOr[Link]] | None = None + callbacks: dict[UntrustedString, ReferenceOr[Callback]] | None = None model_config = ConfigDict( # `Callback` contains an unresolvable forward reference, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/contact.py b/openapi_python_client/schema/openapi_schema_pydantic/contact.py index 0ff40fa71..ea7cbe33d 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/contact.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/contact.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from openapi_python_client.schema.untrusted_string import UntrustedString + class Contact(BaseModel): """ @@ -9,9 +11,9 @@ class Contact(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject """ - name: str | None = None - url: str | None = None - email: str | None = None + name: UntrustedString | None = None + url: UntrustedString | None = None + email: UntrustedString | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py b/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py index 3fb05ade9..3a10ce7f3 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class Discriminator(BaseModel): """ @@ -16,8 +18,8 @@ class Discriminator(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject """ - propertyName: str - mapping: dict[str, str] | None = None + propertyName: UntrustedString + mapping: dict[UntrustedString, UntrustedString] | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/encoding.py b/openapi_python_client/schema/openapi_schema_pydantic/encoding.py index d2725e9f7..f09a7602e 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/encoding.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .reference import ReferenceOr if TYPE_CHECKING: # pragma: no cover @@ -16,9 +17,9 @@ class Encoding(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject """ - contentType: str | None = None - headers: dict[str, ReferenceOr["Header"]] | None = None - style: str | None = None + contentType: UntrustedString | None = None + headers: dict[UntrustedString, ReferenceOr["Header"]] | None = None + style: UntrustedString | None = None explode: bool = False allowReserved: bool = False model_config = ConfigDict( diff --git a/openapi_python_client/schema/openapi_schema_pydantic/example.py b/openapi_python_client/schema/openapi_schema_pydantic/example.py index 6469457f7..bd3d188aa 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/example.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/example.py @@ -2,6 +2,8 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class Example(BaseModel): """Examples added to parameters / components to help clarify usage. @@ -11,10 +13,10 @@ class Example(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject """ - summary: str | None = None - description: str | None = None + summary: UntrustedString | None = None + description: UntrustedString | None = None value: Any | None = None - externalValue: str | None = None + externalValue: UntrustedString | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py b/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py index 78df42f67..b5565beb8 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from openapi_python_client.schema.untrusted_string import UntrustedString + class ExternalDocumentation(BaseModel): """Allows referencing an external resource for extended documentation. @@ -8,8 +10,8 @@ class ExternalDocumentation(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#externalDocumentationObject """ - description: str | None = None - url: str + description: UntrustedString | None = None + url: UntrustedString model_config = ConfigDict( extra="allow", json_schema_extra={"examples": [{"description": "Find more info here", "url": "https://example.com"}]}, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/header.py b/openapi_python_client/schema/openapi_schema_pydantic/header.py index 2deb6f390..e81300459 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/header.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/header.py @@ -1,10 +1,15 @@ -from pydantic import ConfigDict, Field +from typing import Any -from ..parameter_location import ParameterLocation -from .parameter import Parameter +from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString +from .example import Example +from .media_type import MediaType +from .reference import ReferenceOr +from .schema import Schema -class Header(Parameter): + +class Header(BaseModel): """ The Header Object follows the structure of the [Parameter Object](#parameterObject) with the following changes: @@ -18,8 +23,17 @@ class Header(Parameter): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject """ - name: str = Field(default="") - param_in: ParameterLocation = Field(default=ParameterLocation.HEADER, alias="in") + description: UntrustedString | None = None + required: bool = False + deprecated: bool = False + allowEmptyValue: bool = False + style: UntrustedString | None = None + explode: bool = False + allowReserved: bool = False + param_schema: ReferenceOr[Schema] | None = Field(default=None, alias="schema") + example: Any | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + content: dict[UntrustedString, MediaType] | None = None model_config = ConfigDict( # `Parameter` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/info.py b/openapi_python_client/schema/openapi_schema_pydantic/info.py index 8b3e59963..316ed538e 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/info.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/info.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .contact import Contact from .license import License @@ -15,12 +16,12 @@ class Info(BaseModel): -https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject """ - title: str - description: str | None = None - termsOfService: str | None = None + title: UntrustedString + description: UntrustedString | None = None + termsOfService: UntrustedString | None = None contact: Contact | None = None license: License | None = None - version: str + version: UntrustedString model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/license.py b/openapi_python_client/schema/openapi_schema_pydantic/license.py index ee712d92e..1c40b285a 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/license.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/license.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class License(BaseModel): """ @@ -9,8 +11,8 @@ class License(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject """ - name: str - url: str | None = None + name: UntrustedString + url: UntrustedString | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/link.py b/openapi_python_client/schema/openapi_schema_pydantic/link.py index 77740ceb4..feccecccc 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/link.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/link.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .server import Server @@ -23,11 +24,11 @@ class Link(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#linkObject """ - operationRef: str | None = None - operationId: str | None = None - parameters: dict[str, Any] | None = None + operationRef: UntrustedString | None = None + operationId: UntrustedString | None = None + parameters: dict[UntrustedString, Any] | None = None requestBody: Any | None = None - description: str | None = None + description: UntrustedString | None = None server: Server | None = None model_config = ConfigDict( extra="allow", diff --git a/openapi_python_client/schema/openapi_schema_pydantic/media_type.py b/openapi_python_client/schema/openapi_schema_pydantic/media_type.py index 1f4e6090f..7418b9715 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/media_type.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString from .encoding import Encoding from .example import Example from .reference import ReferenceOr @@ -18,8 +19,8 @@ class MediaType(BaseModel): media_type_schema: ReferenceOr[Schema] | None = Field(default=None, alias="schema") example: Any | None = None - examples: dict[str, ReferenceOr[Example]] | None = None - encoding: dict[str, Encoding] | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + encoding: dict[UntrustedString, Encoding] | None = None model_config = ConfigDict( # `Encoding` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py index d9066e383..8320c148a 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class OAuthFlow(BaseModel): """ @@ -10,10 +12,10 @@ class OAuthFlow(BaseModel): - https://swagger.io/docs/specification/authentication/oauth2/ """ - authorizationUrl: str | None = None - tokenUrl: str | None = None - refreshUrl: str | None = None - scopes: dict[str, str] + authorizationUrl: UntrustedString | None = None + tokenUrl: UntrustedString | None = None + refreshUrl: UntrustedString | None = None + scopes: dict[UntrustedString, UntrustedString] model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/open_api.py b/openapi_python_client/schema/openapi_schema_pydantic/open_api.py index a98158232..890cf5793 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/open_api.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict, field_validator +from ..untrusted_string import UntrustedString from .components import Components from .external_documentation import ExternalDocumentation from .info import Info @@ -26,7 +27,7 @@ class OpenAPI(BaseModel): security: list[SecurityRequirement] | None = None tags: list[Tag] | None = None externalDocs: ExternalDocumentation | None = None - openapi: str + openapi: UntrustedString model_config = ConfigDict( # `Components` is not build yet, will rebuild in `__init__.py`: defer_build=True, @@ -35,8 +36,9 @@ class OpenAPI(BaseModel): @field_validator("openapi") @classmethod - def check_openapi_version(cls, value: str) -> str: + def check_openapi_version(cls, untrusted: UntrustedString) -> UntrustedString: """Validates that the declared OpenAPI version is a supported one""" + value = untrusted.get_untrusted_value() parts = value.split(".") if len(parts) != NUM_SEMVER_PARTS: raise ValueError(f"Invalid OpenAPI version {value}") @@ -44,4 +46,4 @@ def check_openapi_version(cls, value: str) -> str: raise ValueError(f"Only OpenAPI versions 3.* are supported, got {value}") if int(parts[1]) > 1: raise ValueError(f"Only OpenAPI versions 3.1.* are supported, got {value}") - return value + return untrusted diff --git a/openapi_python_client/schema/openapi_schema_pydantic/operation.py b/openapi_python_client/schema/openapi_schema_pydantic/operation.py index 061be3135..9bc1dbef3 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/operation.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/operation.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString from .callback import Callback from .external_documentation import ExternalDocumentation from .parameter import Parameter @@ -18,15 +19,15 @@ class Operation(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operationObject """ - tags: list[str] | None = None - summary: str | None = None - description: str | None = None + tags: list[UntrustedString] | None = None + summary: UntrustedString | None = None + description: UntrustedString | None = None externalDocs: ExternalDocumentation | None = None - operationId: str | None = None + operationId: UntrustedString | None = None parameters: list[ReferenceOr[Parameter]] | None = None request_body: ReferenceOr[RequestBody] | None = Field(None, alias="requestBody") responses: Responses - callbacks: dict[str, Callback] | None = None + callbacks: dict[UntrustedString, Callback] | None = None deprecated: bool = False security: list[SecurityRequirement] | None = None diff --git a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py index a963291fc..880cc6560 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, ConfigDict, Field from ..parameter_location import ParameterLocation +from ..untrusted_string import UntrustedString from .example import Example from .media_type import MediaType from .reference import ReferenceOr @@ -21,19 +22,19 @@ class Parameter(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject """ - name: str + name: UntrustedString param_in: ParameterLocation = Field(alias="in") - description: str | None = None + description: UntrustedString | None = None required: bool = False deprecated: bool = False allowEmptyValue: bool = False - style: str | None = None + style: UntrustedString | None = None explode: bool = False allowReserved: bool = False param_schema: ReferenceOr[Schema] | None = Field(default=None, alias="schema") example: Any | None = None - examples: dict[str, ReferenceOr[Example]] | None = None - content: dict[str, MediaType] | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + content: dict[UntrustedString, MediaType] | None = None model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py index 16fee9aeb..5e1116f3d 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py @@ -2,6 +2,8 @@ from pydantic import BaseModel, ConfigDict, Field +from ..ref import Ref +from ..untrusted_string import UntrustedString from .parameter import Parameter from .reference import ReferenceOr from .server import Server @@ -22,9 +24,9 @@ class PathItem(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject """ - ref: str | None = Field(default=None, alias="$ref") - summary: str | None = None - description: str | None = None + ref: Ref | None = Field(default=None, alias="$ref") + summary: UntrustedString | None = None + description: UntrustedString | None = None get: "Operation | None" = None put: "Operation | None" = None post: "Operation | None" = None diff --git a/openapi_python_client/schema/openapi_schema_pydantic/paths.py b/openapi_python_client/schema/openapi_schema_pydantic/paths.py index 86c1dfd19..bc7181edf 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/paths.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/paths.py @@ -1,6 +1,7 @@ +from ..untrusted_string import UntrustedString from .path_item import PathItem -Paths = dict[str, PathItem] +Paths = dict[UntrustedString, PathItem] """ Holds the relative paths to the individual endpoints and their operations. The path is appended to the URL from the [`Server Object`](#serverObject) in order to construct the full URL. diff --git a/openapi_python_client/schema/openapi_schema_pydantic/reference.py b/openapi_python_client/schema/openapi_schema_pydantic/reference.py index c1b41e473..c62a7538d 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/reference.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/reference.py @@ -2,6 +2,8 @@ from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag +from openapi_python_client.schema.ref import Ref + class Reference(BaseModel): """ @@ -18,7 +20,7 @@ class Reference(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject """ - ref: str = Field(alias="$ref") + ref: Ref = Field(alias="$ref") model_config = ConfigDict( extra="allow", populate_by_name=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/request_body.py b/openapi_python_client/schema/openapi_schema_pydantic/request_body.py index 60c454b6d..361ac8102 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/request_body.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .media_type import MediaType @@ -11,8 +12,8 @@ class RequestBody(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject """ - description: str | None = None - content: dict[str, MediaType] + description: UntrustedString | None = None + content: dict[UntrustedString, MediaType] required: bool = False model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: diff --git a/openapi_python_client/schema/openapi_schema_pydantic/response.py b/openapi_python_client/schema/openapi_schema_pydantic/response.py index e6d8ebb2c..ae972c16c 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/response.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/response.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .header import Header from .link import Link from .media_type import MediaType @@ -16,10 +17,10 @@ class Response(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject """ - description: str - headers: dict[str, ReferenceOr[Header]] | None = None - content: dict[str, MediaType] | None = None - links: dict[str, ReferenceOr[Link]] | None = None + description: UntrustedString + headers: dict[UntrustedString, ReferenceOr[Header]] | None = None + content: dict[UntrustedString, MediaType] | None = None + links: dict[UntrustedString, ReferenceOr[Link]] | None = None model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/responses.py b/openapi_python_client/schema/openapi_schema_pydantic/responses.py index 823339a54..9714bd535 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/responses.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/responses.py @@ -1,7 +1,8 @@ +from ..untrusted_string import UntrustedString from .reference import ReferenceOr from .response import Response -Responses = dict[str, ReferenceOr[Response]] +Responses = dict[UntrustedString, ReferenceOr[Response]] """ A container for the expected responses of an operation. The container maps a HTTP response code to the expected response. diff --git a/openapi_python_client/schema/openapi_schema_pydantic/schema.py b/openapi_python_client/schema/openapi_schema_pydantic/schema.py index 990f14a3d..39ee4ffd8 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/schema.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/schema.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, model_validator from ..data_type import DataType +from ..untrusted_string import UntrustedString from .discriminator import Discriminator from .external_documentation import ExternalDocumentation from .reference import ReferenceOr @@ -20,7 +21,7 @@ class Schema(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject """ - title: str | None = None + title: UntrustedString | None = None multipleOf: float | None = Field(default=None, gt=0.0) maximum: float | None = None exclusiveMaximum: bool | float | None = None @@ -28,13 +29,13 @@ class Schema(BaseModel): exclusiveMinimum: bool | float | None = None maxLength: int | None = Field(default=None, ge=0) minLength: int | None = Field(default=None, ge=0) - pattern: str | None = None + pattern: UntrustedString | None = None maxItems: int | None = Field(default=None, ge=0) minItems: int | None = Field(default=None, ge=0) uniqueItems: bool | None = None maxProperties: int | None = Field(default=None, ge=0) minProperties: int | None = Field(default=None, ge=0) - required: list[str] | None = Field(default=None) + required: list[UntrustedString] | None = Field(default=None) enum: None | list[Any] = Field(default=None, min_length=1) const: None | StrictStr | StrictInt | StrictFloat | StrictBool = None type: DataType | list[DataType] | None = Field(default=None) @@ -44,10 +45,10 @@ class Schema(BaseModel): schema_not: ReferenceOr["Schema"] | None = Field(default=None, alias="not") items: ReferenceOr["Schema"] | None = None prefixItems: list[ReferenceOr["Schema"]] = Field(default_factory=list) - properties: dict[str, ReferenceOr["Schema"]] | None = None + properties: dict[UntrustedString, ReferenceOr["Schema"]] | None = None additionalProperties: bool | ReferenceOr["Schema"] | None = None - description: str | None = None - schema_format: str | None = Field(default=None, alias="format") + description: UntrustedString | None = None + schema_format: UntrustedString | None = Field(default=None, alias="format") default: Any | None = None nullable: bool = Field(default=False) discriminator: Discriminator | None = None diff --git a/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py b/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py index 58a487dc7..c299895ce 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py @@ -1,4 +1,6 @@ -SecurityRequirement = dict[str, list[str]] +from ..untrusted_string import UntrustedString + +SecurityRequirement = dict[UntrustedString, list[UntrustedString]] """ Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the diff --git a/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py b/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py index 76b97bd90..77c681c59 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString from .oauth_flows import OAuthFlows @@ -17,14 +18,14 @@ class SecurityScheme(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject """ - type: str - description: str | None = None - name: str | None = None - security_scheme_in: str | None = Field(default=None, alias="in") - scheme: str | None = None - bearerFormat: str | None = None + type: UntrustedString + description: UntrustedString | None = None + name: UntrustedString | None = None + security_scheme_in: UntrustedString | None = Field(default=None, alias="in") + scheme: UntrustedString | None = None + bearerFormat: UntrustedString | None = None flows: OAuthFlows | None = None - openIdConnectUrl: str | None = None + openIdConnectUrl: UntrustedString | None = None model_config = ConfigDict( extra="allow", populate_by_name=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/server.py b/openapi_python_client/schema/openapi_schema_pydantic/server.py index abfb77e1b..a975dcf06 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/server.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/server.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .server_variable import ServerVariable @@ -11,9 +12,9 @@ class Server(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject """ - url: str - description: str | None = None - variables: dict[str, ServerVariable] | None = None + url: UntrustedString + description: UntrustedString | None = None + variables: dict[UntrustedString, ServerVariable] | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py b/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py index 09a705a2b..4517783bc 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class ServerVariable(BaseModel): """An object representing a Server Variable for server URL template substitution. @@ -9,7 +11,7 @@ class ServerVariable(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverVariableObject """ - enum: list[str] | None = None - default: str - description: str | None = None + enum: list[UntrustedString] | None = None + default: UntrustedString + description: UntrustedString | None = None model_config = ConfigDict(extra="allow") diff --git a/openapi_python_client/schema/openapi_schema_pydantic/tag.py b/openapi_python_client/schema/openapi_schema_pydantic/tag.py index 799810f36..a433988b5 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/tag.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/tag.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .external_documentation import ExternalDocumentation @@ -13,8 +14,8 @@ class Tag(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject """ - name: str - description: str | None = None + name: UntrustedString + description: UntrustedString | None = None externalDocs: ExternalDocumentation | None = None model_config = ConfigDict( extra="allow", json_schema_extra={"examples": [{"name": "pet", "description": "Pets operations"}]} diff --git a/openapi_python_client/schema/openapi_schema_pydantic/xml.py b/openapi_python_client/schema/openapi_schema_pydantic/xml.py index 87082b3c8..53b178aa9 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/xml.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/xml.py @@ -1,5 +1,7 @@ from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class XML(BaseModel): """ @@ -14,9 +16,9 @@ class XML(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject """ - name: str | None = None - namespace: str | None = None - prefix: str | None = None + name: UntrustedString | None = None + namespace: UntrustedString | None = None + prefix: UntrustedString | None = None attribute: bool = False wrapped: bool = False model_config = ConfigDict( diff --git a/openapi_python_client/schema/ref.py b/openapi_python_client/schema/ref.py new file mode 100644 index 000000000..35d228055 --- /dev/null +++ b/openapi_python_client/schema/ref.py @@ -0,0 +1,52 @@ +from typing import Any + +from pydantic import GetCoreSchemaHandler +from pydantic_core import core_schema + +from .untrusted_string import UntrustedString + + +class Ref: + """A $ref in the schema, the raw value is protected to make sure we don't accidentally place it in the generated code.""" + + def __init__(self, value: str | UntrustedString) -> None: + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + self._value = value + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.no_info_after_validator_function( + cls, + core_schema.str_schema(), + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, info_arg=True, return_schema=core_schema.any_schema() + ), + ) + + @staticmethod + def _serialize(value: "Ref", info: core_schema.SerializationInfo) -> Any: + # Only unwrap in JSON mode (used for error messages); Python mode keeps the wrapper so it + # can't leak into generated code. Returning the wrapper also avoids serializer warnings. + if info.mode == "json": + return value.get_untrusted_value() + return value + + def get_untrusted_value(self) -> str: + """ + Get the raw $ref value. + + DO NOT place this in generated code. + """ + return self._value + + def endswith(self, suffix: str) -> bool: + return self._value.endswith(suffix) + + def __hash__(self) -> int: + return hash(self._value) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Ref): + return self._value == other._value + return False # pragma: no cover diff --git a/openapi_python_client/schema/untrusted_string.py b/openapi_python_client/schema/untrusted_string.py new file mode 100644 index 000000000..e54bf2858 --- /dev/null +++ b/openapi_python_client/schema/untrusted_string.py @@ -0,0 +1,56 @@ +from typing import Any, TypeVar + +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, core_schema + +T = TypeVar("T") + + +class UntrustedString: + """A raw string in the OpenAPI document which should not be directly placed in the generated code.""" + + def __init__(self, value: str) -> None: + self._value = value + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema: + return core_schema.no_info_after_validator_function( + cls, + core_schema.str_schema(), + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, info_arg=True, return_schema=core_schema.any_schema() + ), + ) + + @staticmethod + def _serialize(value: "UntrustedString", info: core_schema.SerializationInfo) -> Any: + # Only unwrap in JSON mode (used for error messages); Python mode keeps the wrapper so it + # can't leak into generated code. Returning the wrapper also avoids serializer warnings. + if info.mode == "json": + return value.get_untrusted_value() + return value + + def get_untrusted_value(self) -> str: + """ + Return the raw string with no transformations. + + DO NOT use this for generating code directly. It can result in arbitrary code generation. + """ + return self._value + + def startswith(self, prefix: str) -> bool: + return self._value.startswith(prefix) + + def __bool__(self) -> bool: + """Whether the raw value is non-empty. Emptiness is not sensitive, so this is safe to expose.""" + return bool(self._value) + + def __hash__(self) -> int: + return hash(self._value) + + def __eq__(self, other: object) -> bool: + if isinstance(other, UntrustedString): + return self._value == other._value + if isinstance(other, str): + return self._value == other + return False diff --git a/openapi_python_client/strings.py b/openapi_python_client/strings.py new file mode 100644 index 000000000..bfbfc9e4f --- /dev/null +++ b/openapi_python_client/strings.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +import builtins +import json +import re +import unicodedata +from dataclasses import dataclass +from email.message import Message +from keyword import iskeyword +from typing import Any + +from .config import Config +from .schema.untrusted_string import UntrustedString + +DELIMITERS = r"\. _-" + + +@dataclass(frozen=True, repr=False, slots=True) +class PythonCode: + """ + A snippet of Python source code generated from `UnstrustedString`s. + + It's safe to treat as code, but _may not_ be safe to place in docstrings, f-strings, etc. + """ + + _code: str + + def as_unembedded_code(self) -> str: + """ + Return the raw source code with no transformations. + + DO NOT use this inside a string context (docstrings, string literals, f-string literal text). + Pass this through the appropriate escaping function first (e.g. `safe_for_docstring`). + """ + return self._code + + +class PythonIdentifier(str): + """A snake_case string which has been validated / transformed into a valid identifier for Python""" + + def __new__(cls, value: UntrustedString | str, prefix: str, skip_snake_case: bool = False) -> PythonIdentifier: + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + safe_value = sanitize(value) + leading_underscore = safe_value.startswith("_") + if skip_snake_case: + # Keep the name as close to the original as possible (to disambiguate conflicts); + # only strip the delimiters sanitize() preserved, as they aren't valid in identifiers + safe_value = re.sub(r"[^\w]+", "", safe_value) + else: + safe_value = snake_case(safe_value) + safe_value = fix_reserved_words(safe_value) + + if not safe_value.isidentifier() or leading_underscore: + safe_value = f"{prefix}{safe_value}" + return str.__new__(cls, safe_value) + + def __deepcopy__(self, _: Any) -> PythonIdentifier: + return self + + +class ClassName(str): + """A PascalCase string which has been validated / transformed into a valid class name for Python""" + + def __new__(cls, value: UntrustedString | str, prefix: str) -> ClassName: + safe_value = fix_reserved_words(pascal_case(value)) + + if not safe_value.isidentifier(): + safe_value = f"{prefix}{safe_value}" + safe_value = fix_reserved_words(pascal_case(safe_value)) + return str.__new__(cls, safe_value) + + def __deepcopy__(self, _: Any) -> ClassName: + return self + + +def sanitize(value: str) -> str: + """Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter""" + return "".join(c for c in value if c.isidentifier() or c.isdecimal() or c in DELIMITERS) + + +def split_words(value: str) -> list[str]: + """Split a string on words and known delimiters""" + if any(c.isupper() for c in value): + value = " ".join(re.split("([A-Z]?[a-z]+)", value)) + return re.findall(rf"[^{re.escape(DELIMITERS)}]+", value) + + +RESERVED_WORDS = (set(dir(builtins)) | {"self", "true", "false", "datetime"}) - { + "id", +} + + +def fix_reserved_words(value: str) -> str: + """ + Using reserved Python words as identifiers in generated code causes problems, so this function renames them. + + Args: + value: The identifier to-be that should be renamed if it's a reserved word. + + Returns: + `value` suffixed with `_` if it was a reserved word. + """ + if value in RESERVED_WORDS or iskeyword(value): + return f"{value}_" + return value + + +def snake_case(value: str | UntrustedString) -> str: + """Converts to snake_case identifier, stripping all non-alphanumeric characters""" + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + words = split_words(sanitize(value)) + return "_".join(words).lower() + + +def pascal_case(value: str | UntrustedString) -> str: + """Converts to PascalCase identifier, stripping all non-alphanumeric characters""" + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + words = split_words(sanitize(value)) + capitalized_words = (word.capitalize() if not word.isupper() else word for word in words) + return "".join(capitalized_words) + + +def kebab_case(value: str | UntrustedString) -> str: + """Converts to kebab-case identifier, stripping all non-alphanumeric characters""" + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + words = split_words(sanitize(value)) + return "-".join(words).lower() + + +#: Escaped words in docstring content are kept shorter than this so the ``wordwrap`` filter +#: never hard-splits inside an escape sequence (the narrowest wordwrap width in templates is 90). +_MAX_ESCAPED_WORD_LENGTH = 76 + + +def _split_long_word_safely(word: str) -> str: + """Insert newlines into an over-long escaped word where they can't interrupt an escape sequence. + + wordwrap hard-splits words longer than its width at arbitrary positions. A split between the + backslashes of ``\\\\`` leaves a lone backslash at the start of the next line, where it forms an + invalid escape (``\\ `` warns today and will error in a future Python) or even a ``SyntaxError`` + (``\\U`` + non-hex is a truncated escape). Splits right after an even number of backslashes are + always safe: the prefix ends with complete escapes and the suffix can't start a dangling one. + """ + chunks: list[str] = [] + start = 0 + while len(word) - start > _MAX_ESCAPED_WORD_LENGTH: + end = start + _MAX_ESCAPED_WORD_LENGTH + run = 0 + while end > start and word[end - 1] == "\\": + end -= 1 + run += 1 + # Keep an even number of trailing backslashes in this chunk. Odd runs only occur right + # before an escaped quote, so the one left behind always forms a valid `\"`. + end += run - (run % 2) + chunks.append(word[start:end]) + start = end + chunks.append(word[start:]) + return "\n".join(chunks) + + +def safe_for_docstring(value: Any) -> str: + """ + Remove any risky escapes from docstring content. + + The output is safe to run through the ``wordwrap`` filter at any width greater than + ``_MAX_ESCAPED_WORD_LENGTH``: over-long words containing escape sequences are pre-split where + no escape sequence is interrupted, so wordwrap's hard-splitting can't create invalid escapes. + """ + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + if isinstance(value, PythonCode): + value = value.as_unembedded_code() + if not isinstance(value, str): + value = str(value) + # Strip control characters which are illegal in Python source (e.g. null bytes) + value = "".join(c for c in value if unicodedata.category(c) != "Cc" or c in "\n\r\t") + + value = value.replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + # Words without backslashes can be hard-split anywhere safely, so leave them alone + return re.sub( + r"\S+", + lambda match: _split_long_word_safely(match.group()) if "\\" in match.group() else match.group(), + value, + ) + + +def in_double_quote_literal(value: UntrustedString | PythonCode | str) -> str: + """Escape a string so it can be safely embedded inside a double-quoted string literal. + + The result does NOT include surrounding quotes; the caller is expected to place it inside its own `"..."` + (e.g. a Python string literal or a TOML string in generated code). JSON encoding is used for the escaping, + which is valid inside both Python and TOML double-quoted strings. + """ + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + if isinstance(value, PythonCode): + value = value.as_unembedded_code() + value = "".join( + character + for character in value + if (character in "\b\t\f" or not unicodedata.category(character).startswith("C")) + and unicodedata.category(character) not in {"Zl", "Zp"} + ) + return json.dumps(value, ensure_ascii=False)[1:-1] + + +def in_f_string_literal(value: UntrustedString | PythonCode | str) -> str: + """Escape a string so it can be safely embedded as literal text inside a double-quoted f-string. + + In addition to the double-quoted-string escaping (quotes, backslashes), this escapes `{` and `}` as + `{{` and `}}` so the value cannot be interpreted as an f-string replacement field or format specifier. + Without this, an untrusted value could inject arbitrary expressions into the generated f-string. + """ + return in_double_quote_literal(value).replace("{", "{{").replace("}", "}}") + + +def remove_string_escapes(value: str) -> str: + """Used when parsing string-literal defaults to prevent escaping the string to write arbitrary Python + + **REMOVING OR CHANGING THE USAGE OF THIS FUNCTION HAS SECURITY IMPLICATIONS** + + See Also: + - https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-9x4c-63pf-525f + """ + return value.replace("\\", "\\\\").replace('"', r"\"") + + +def get_content_type(content_type: str, config: Config) -> str | None: + """ + Given a string representing a content type with optional parameters, returns the content type only + """ + content_type = config.content_type_overrides.get(content_type, content_type) + message = Message() + message.add_header("Content-Type", content_type) + + parsed_content_type = message.get_content_type() + if not content_type.startswith(parsed_content_type): + # Always defaults to `text/plain` if it's not recognized. We want to return an error, not default. + return None + + return parsed_content_type diff --git a/openapi_python_client/templates/README.md.jinja b/openapi_python_client/templates/README.md.jinja index 66a9c1669..d4b60f303 100644 --- a/openapi_python_client/templates/README.md.jinja +++ b/openapi_python_client/templates/README.md.jinja @@ -1,5 +1,5 @@ # {{ project_name }} -{{ package_description }} +{{ package_description.get_untrusted_value() }} ## Usage First, create a client: diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 9688a4ba2..493c3551c 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -1,5 +1,5 @@ {% from "property_templates/helpers.jinja" import guarded_statement %} -{% from "helpers.jinja" import safe_docstring %} +{% from "helpers.jinja" import docstring as _docstring %} {% macro header_params(endpoint) %} {% if endpoint.header_parameters or endpoint.bodies | length > 0 %} @@ -12,7 +12,7 @@ headers: dict[str, Any] = {} {% else %} {% set expression = parameter.python_name %} {% endif %} - {% set statement = 'headers["' + parameter.name + '"]' + " = " + expression %} + {% set statement = 'headers["' + (parameter.name | in_double_quote_literal) + '"]' + " = " + expression %} {{ guarded_statement(parameter, parameter.python_name, statement) }} {% endfor %} {% endif %} @@ -24,10 +24,10 @@ headers: dict[str, Any] = {} cookies = {} {% for parameter in endpoint.cookie_parameters %} {% if parameter.required %} -cookies["{{ parameter.name}}"] = {{ parameter.python_name }} +cookies["{{ parameter.name | in_double_quote_literal }}"] = {{ parameter.python_name }} {% else %} if {{ parameter.python_name }} is not UNSET: - cookies["{{ parameter.name}}"] = {{ parameter.python_name }} + cookies["{{ parameter.name | in_double_quote_literal }}"] = {{ parameter.python_name }} {% endif %} {% endfor %} @@ -47,7 +47,7 @@ params: dict[str, Any] = {} {{ prop_template.transform(property, property.python_name, destination) }} {% endif %} {%- if not property.json_is_dict %} -params["{{ property.name }}"] = {{ destination }} +params["{{ property.name | in_double_quote_literal }}"] = {{ destination }} {% else %} {{ guarded_statement(property, destination, "params.update(" + destination + ")") }} {% endif %} @@ -105,7 +105,7 @@ if not isinstance({{property.python_name}}, Unset): {% macro arguments(endpoint, include_client=True) %} {# path parameters #} {% for parameter in endpoint.path_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {% if include_client or ((endpoint.list_all_parameters() | length) > (endpoint.path_parameters | length)) %} *, @@ -120,24 +120,24 @@ client: AuthenticatedClient | Client, {% endif %} {# Any allowed bodies #} {% if endpoint.bodies | length == 1 %} -body: {{ endpoint.bodies[0].prop.get_type_string() }}{% if not endpoint.bodies[0].prop.required %} = UNSET{% endif %}, +body: {{ endpoint.bodies[0].prop.get_type_string() | as_unembedded_code }}{% if not endpoint.bodies[0].prop.required %} = UNSET{% endif %}, {% elif endpoint.bodies | length > 1 %} body: {%- for body in endpoint.bodies -%}{% set body_required = body_required and body.prop.required %} - {{ body.prop.get_type_string(no_optional=True) }} {% if not loop.last %} | {% endif %} + {{ body.prop.get_type_string(no_optional=True) | as_unembedded_code }} {% if not loop.last %} | {% endif %} {%- endfor -%}{% if not body_required %} | Unset = UNSET{% endif %} , {% endif %} {# query parameters #} {% for parameter in endpoint.query_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {% for parameter in endpoint.header_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {# cookie parameters #} {% for parameter in endpoint.cookie_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {% endmacro %} @@ -164,10 +164,10 @@ body=body, {% endmacro %} {% macro docstring_content(endpoint, return_string, is_detailed) %} -{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}} +{% if endpoint.summary %}{{ endpoint.summary | safe_for_docstring | wordwrap(100)}} {% endif -%} -{%- if endpoint.description %} {{ endpoint.description | wordwrap(100) }} +{%- if endpoint.description %} {{ endpoint.description | safe_for_docstring | wordwrap(100) }} {% endif %} {% if not endpoint.summary and not endpoint.description %} @@ -188,24 +188,24 @@ Raises: Returns: {% if is_detailed %} - Response[{{ return_string }}] + Response[{{ return_string | safe_for_docstring }}] {% else %} - {{ return_string }} + {{ return_string | safe_for_docstring }} {% endif %} {% endmacro %} {% macro docstring(endpoint, return_string, is_detailed) %} -{{ safe_docstring(docstring_content(endpoint, return_string, is_detailed)) }} +{{ _docstring(docstring_content(endpoint, return_string, is_detailed)) }} {% endmacro %} {% macro parse_response(parsed_responses, response) %} {% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} {% if prop_template.construct %} {{ prop_template.construct(response.prop, response.source.attribute) }} -{% elif response.source.return_type == response.prop.get_type_string() %} +{% elif response.source.return_type == response.prop.get_type_string() | as_unembedded_code %} {{ response.prop.python_name }} = {{ response.source.attribute }} {% else %} -{{ response.prop.python_name }} = cast({{ response.prop.get_type_string() }}, {{ response.source.attribute }}) +{{ response.prop.python_name }} = cast({{ response.prop.get_type_string() | as_unembedded_code }}, {{ response.source.attribute }}) {% endif %} return {{ response.prop.python_name }} {% else %} diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 63e617778..97f30a2b8 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -16,7 +16,7 @@ from ... import errors arguments, client, kwargs, parse_response, docstring, body_to_kwarg %} {% set return_string = endpoint.response_type() %} -{% set parsed_responses = (endpoint.responses | length > 0) and return_string != "Any" %} +{% set parsed_responses = (endpoint.responses | length > 0) and return_string != PythonCode("Any") %} def _get_kwargs( {{ arguments(endpoint, include_client=False) | indent(4) }} @@ -30,13 +30,13 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "{{ endpoint.method }}", {% if endpoint.path_parameters %} - "url": "{{ endpoint.path }}".format( + "url": "{{ endpoint.path | in_double_quote_literal }}".format( {%- for parameter in endpoint.path_parameters -%} {{parameter.python_name}}=quote(str({{parameter.python_name}}), safe=""), {%- endfor -%} ), {% else %} - "url": "{{ endpoint.path }}", + "url": "{{ endpoint.path | in_double_quote_literal }}", {% endif %} {% if endpoint.query_parameters %} "params": params, @@ -48,12 +48,12 @@ def _get_kwargs( {% if endpoint.bodies | length > 1 %} {% for body in endpoint.bodies %} - if isinstance(body, {{body.prop.get_type_string(no_optional=True) }}): + if isinstance(body, {{body.prop.get_type_string(no_optional=True) | as_unembedded_code }}): {{ body_to_kwarg(body) | indent(8) }} {%- if body.content_type == "multipart/form-data" %} headers["Content-Type"] = "multipart/form-data; boundary=+++" {% else %} - headers["Content-Type"] = "{{ body.content_type }}" + headers["Content-Type"] = "{{ body.content_type | in_double_quote_literal }}" {% endif %} {% endfor %} {% elif endpoint.bodies | length == 1 %} @@ -62,7 +62,7 @@ def _get_kwargs( {%- if body.content_type == "multipart/form-data" %} headers["Content-Type"] = "multipart/form-data; boundary=+++" {% else %} - headers["Content-Type"] = "{{ body.content_type }}" + headers["Content-Type"] = "{{ body.content_type | in_double_quote_literal }}" {% endif %} {% endif %} @@ -72,9 +72,9 @@ def _get_kwargs( return _kwargs {% if endpoint.responses.default %} - {% set return_type = return_string %} + {% set return_type = return_string | as_unembedded_code %} {% else %} - {% set return_type = return_string + " | None" %} + {% set return_type = (return_string | as_unembedded_code) + " | None" %} {% endif %} @@ -98,7 +98,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res {% endif %} -def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[{{ return_string }}]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[{{ return_string | as_unembedded_code }}]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -109,7 +109,7 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( {{ arguments(endpoint) | indent(4) }} -) -> Response[{{ return_string }}]: +) -> Response[{{ return_string | as_unembedded_code }}]: {{ docstring(endpoint, return_string, is_detailed=true) | indent(4) }} kwargs = _get_kwargs( @@ -125,7 +125,7 @@ def sync_detailed( {% if parsed_responses %} def sync( {{ arguments(endpoint) | indent(4) }} -) -> {{ return_string }} | None: +) -> {{ return_string | as_unembedded_code }} | None: {{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }} return sync_detailed( @@ -135,7 +135,7 @@ def sync( async def asyncio_detailed( {{ arguments(endpoint) | indent(4) }} -) -> Response[{{ return_string }}]: +) -> Response[{{ return_string | as_unembedded_code }}]: {{ docstring(endpoint, return_string, is_detailed=true) | indent(4) }} kwargs = _get_kwargs( @@ -151,7 +151,7 @@ async def asyncio_detailed( {% if parsed_responses %} async def asyncio( {{ arguments(endpoint) | indent(4) }} -) -> {{ return_string }} | None: +) -> {{ return_string | as_unembedded_code }} | None: {{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }} return (await asyncio_detailed( diff --git a/openapi_python_client/templates/helpers.jinja b/openapi_python_client/templates/helpers.jinja index fd5c3ec86..4302dbc09 100644 --- a/openapi_python_client/templates/helpers.jinja +++ b/openapi_python_client/templates/helpers.jinja @@ -1,10 +1,6 @@ -{% macro safe_docstring(content, omit_if_empty=False) %} -{# This macro returns the provided content as a docstring, set to a raw string if it contains a backslash #} -{% if (not omit_if_empty) or (content | trim) %} -{% if '\\' in content -%} -r""" {{ content }} """ -{%- else -%} +{% macro docstring(content) %} +{# This macro returns the provided content as a docstring unless it's empty, then it emits nothing. #} +{% if (content | trim) %} """ {{ content }} """ {%- endif -%} -{% endif %} {% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/literal_enum.py.jinja b/openapi_python_client/templates/literal_enum.py.jinja index a4529292b..852c533bc 100644 --- a/openapi_python_client/templates/literal_enum.py.jinja +++ b/openapi_python_client/templates/literal_enum.py.jinja @@ -4,7 +4,7 @@ from typing import Literal {{ enum.get_class_name_snake_case() | upper }}_VALUES: set[{{ enum.class_info.name }}] = { {% for v in enum.values|list|sort %}{{"%r"|format(v)}}, {% endfor %} } -def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() }}) -> {{ enum.class_info.name}}: +def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() | as_unembedded_code }}) -> {{ enum.class_info.name}}: if value in {{ enum.get_class_name_snake_case() | upper }}_VALUES: return value raise TypeError(f"Unexpected value {value!r}. Expected one of {{"{"}}{{ enum.get_class_name_snake_case() | upper }}_VALUES!r}") diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 06a71cb22..141635127 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -25,21 +25,21 @@ if TYPE_CHECKING: {% if model.additional_properties %} -{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string() %} +{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string() | as_unembedded_code %} {% endif %} {% set class_name = model.class_info.name %} {% set module_name = model.class_info.module_name %} -{% from "helpers.jinja" import safe_docstring %} +{% from "helpers.jinja" import docstring %} T = TypeVar("T", bound="{{ class_name }}") {% macro class_docstring_content(model) %} - {% if model.title %}{{ model.title | wordwrap(116) }} + {% if model.title %}{{ model.title | safe_for_docstring | wordwrap(116) }} {% endif -%} - {%- if model.description %}{{ model.description | wordwrap(116) }} + {%- if model.description %}{{ model.description | safe_for_docstring | wordwrap(116) }} {% endif %} {% if not model.title and not model.description %} @@ -48,7 +48,7 @@ T = TypeVar("T", bound="{{ class_name }}") {% endif %} {% if model.example %} Example: - {{ model.example | string | wordwrap(112) | indent(12) }} + {{ model.example | safe_for_docstring | wordwrap(112) | indent(12) }} {% endif %} {% if (not config.docstrings_on_attributes) and (model.required_properties or model.optional_properties) %} @@ -60,16 +60,16 @@ T = TypeVar("T", bound="{{ class_name }}") {% macro declare_property(property) %} {%- if config.docstrings_on_attributes and property.description -%} -{{ property.to_string() }} -{{ safe_docstring(property.description, omit_if_empty=True) | wordwrap(112) }} +{{ property.to_string() | as_unembedded_code }} +{{ docstring(property.description | safe_for_docstring) | wordwrap(112) }} {%- else -%} -{{ property.to_string() }} +{{ property.to_string() | as_unembedded_code }} {%- endif -%} {% endmacro %} @_attrs_define class {{ class_name }}: - {{ safe_docstring(class_docstring_content(model), omit_if_empty=config.docstrings_on_attributes) | indent(4) }} + {{ docstring(class_docstring_content(model)) | indent(4) }} {% for property in model.required_properties + model.optional_properties %} {% if property.default is none and property.required %} @@ -128,7 +128,7 @@ field_dict.update(self.additional_properties) field_dict.update({ {% for property in model.required_properties + model.optional_properties %} {% if property.required %} - "{{ property.name }}": {{ property.python_name }}, + "{{ property.name | in_double_quote_literal }}": {{ property.python_name }}, {% endif %} {% endfor %} }) @@ -136,7 +136,7 @@ field_dict.update({ {% for property in model.optional_properties %} {% if not property.required %} if {{ property.python_name }} is not UNSET: - field_dict["{{ property.name }}"] = {{ property.python_name }} + field_dict["{{ property.name | in_double_quote_literal }}"] = {{ property.python_name }} {% endif %} {% endfor %} @@ -157,7 +157,7 @@ return field_dict files: types.RequestFiles = [] {% for property in model.required_properties + model.optional_properties %} - {% set destination = "\"" + property.name + "\"" %} + {% set destination = "\"" + (property.name | in_double_quote_literal) + "\"" %} {{ multipart(property, "self." + property.python_name, destination) | indent(8) }} {% endfor %} @@ -180,9 +180,9 @@ return field_dict d = dict(src_dict) {% for property in model.required_properties + model.optional_properties %} {% if property.required %} - {% set property_source = 'd.pop("' + property.name + '")' %} + {% set property_source = 'd.pop("' + (property.name | in_double_quote_literal) + '")' %} {% else %} - {% set property_source = 'd.pop("' + property.name + '", UNSET)' %} + {% set property_source = 'd.pop("' + (property.name | in_double_quote_literal) + '", UNSET)' %} {% endif %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.construct %} diff --git a/openapi_python_client/templates/package_init.py.jinja b/openapi_python_client/templates/package_init.py.jinja index ecf60e74d..2854eb6f1 100644 --- a/openapi_python_client/templates/package_init.py.jinja +++ b/openapi_python_client/templates/package_init.py.jinja @@ -1,6 +1,6 @@ -{% from "helpers.jinja" import safe_docstring %} +{% from "helpers.jinja" import docstring %} -{{ safe_docstring(package_description) }} +{{ docstring(package_description | safe_for_docstring) }} from .client import AuthenticatedClient, Client __all__ = ( diff --git a/openapi_python_client/templates/property_templates/const_property.py.jinja b/openapi_python_client/templates/property_templates/const_property.py.jinja index d348de0ff..8be52e752 100644 --- a/openapi_python_client/templates/property_templates/const_property.py.jinja +++ b/openapi_python_client/templates/property_templates/const_property.py.jinja @@ -1,7 +1,7 @@ {% macro construct(property, source) %} -{{ property.python_name }} = cast({{ property.get_type_string() }} , {{ source }}) -if {{ property.python_name }} != {{ property.value.python_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}: - raise ValueError(f"{{ property.name }} must match const {{ property.value.python_code }}, got '{{'{' + property.python_name + '}' }}'") +{{ property.python_name }} = cast({{ property.get_type_string() | as_unembedded_code }} , {{ source }}) +if {{ property.python_name }} != {{ property.value.python_code | as_unembedded_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}: + raise ValueError(f"{{ property.name | in_f_string_literal }} must match const {{ property.value.python_code | in_f_string_literal }}, got '{{'{' + property.python_name + '}' }}'") {%- endmacro %} {% macro multipart(property, source, name) %} diff --git a/openapi_python_client/templates/property_templates/date_property.py.jinja b/openapi_python_client/templates/property_templates/date_property.py.jinja index a11f3d3b1..8bf8d53fa 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -17,7 +17,7 @@ datetime.date.fromisoformat({{ source }}) {%- else %} {% if not skip_unset %} {% if declare_type %} - {% set type_annotation = property.get_type_string(json=True) %} + {% set type_annotation = property.get_type_string(json=True) | as_unembedded_code %} {{ destination }}: {{ type_annotation }} = UNSET {% else %} {{ destination }} = UNSET diff --git a/openapi_python_client/templates/property_templates/datetime_property.py.jinja b/openapi_python_client/templates/property_templates/datetime_property.py.jinja index e669edb08..f8a35e677 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -17,7 +17,7 @@ datetime.datetime.fromisoformat({{ source }}) {%- else %} {% if not skip_unset %} {% if declare_type %} - {% set type_annotation = property.get_type_string(json=True) %} + {% set type_annotation = property.get_type_string(json=True) | as_unembedded_code %} {{ destination }}: {{ type_annotation }} = UNSET {% else %} {{ destination }} = UNSET diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index 19ea03b28..4943ef992 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -12,7 +12,7 @@ {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = source + ".value" %} -{% set type_string = property.get_type_string(json=True) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} diff --git a/openapi_python_client/templates/property_templates/file_property.py.jinja b/openapi_python_client/templates/property_templates/file_property.py.jinja index ac76470a7..df531af81 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -16,7 +16,7 @@ File( {% if property.required %} {{ destination }} = {{ source }}.to_tuple() {% else %} -{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET{% endif +%} +{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) | as_unembedded_code }}{% endif %} = UNSET{% endif +%} if not isinstance({{ source }}, Unset): {{ destination }} = {{ source }}.to_tuple() {% endif %} diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index e765ed3b2..0665412fb 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -11,7 +11,7 @@ for {{ inner_source }} in (_{{ property.python_name }}): {{ property.python_name }}.append({{ inner_property.python_name }}) {% else %} _{{ property.python_name }} = {{ source }} -{{ property.python_name }}: {{ property.get_type_string() }} = UNSET +{{ property.python_name }}: {{ property.get_type_string() | as_unembedded_code }} = UNSET if _{{ property.python_name }} is not UNSET: {{ property.python_name }} = [] for {{ inner_source }} in _{{ property.python_name }}: @@ -19,7 +19,7 @@ if _{{ property.python_name }} is not UNSET: {{ property.python_name }}.append({{ inner_property.python_name }}) {% endif %} {% else %} -{{ property.python_name }} = cast({{ property.get_type_string(no_optional=True) }}, {{ source }}) +{{ property.python_name }} = cast({{ property.get_type_string(no_optional=True) | as_unembedded_code }}, {{ source }}) {% endif %} {% endmacro %} @@ -41,7 +41,7 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set inner_property = property.inner_property %} -{% set type_string = property.get_type_string(json=True) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ _transform(property, source, destination, "to_dict") }} {% else %} diff --git a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja index a0dd3d19b..b718a409b 100644 --- a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja @@ -8,10 +8,10 @@ check_{{ property.get_class_name_snake_case() }}({{ source }}) {{ construct_template(construct_function, property, source) }} {% endmacro %} -{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.get_instance_type_string() }}){% endmacro %} +{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.get_instance_type_string() | as_unembedded_code }}){% endmacro %} {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} -{% set type_string = property.get_type_string(json=True) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = {{ source }} {%- else %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index a442ccb70..629a63c82 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -12,7 +12,7 @@ {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = source + ".to_dict()" %} -{% set type_string = property.get_type_string(json=True) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} diff --git a/openapi_python_client/templates/property_templates/property_macros.py.jinja b/openapi_python_client/templates/property_templates/property_macros.py.jinja index 52e1d41bc..238f8560b 100644 --- a/openapi_python_client/templates/property_templates/property_macros.py.jinja +++ b/openapi_python_client/templates/property_templates/property_macros.py.jinja @@ -3,7 +3,7 @@ {{ property.python_name }} = {{ construct_function(property, source) }} {% else %}{# Must be non-required #} _{{ property.python_name }} = {{ source }} -{{ property.python_name }}: {{ property.get_type_string() }} +{{ property.python_name }}: {{ property.get_type_string() | as_unembedded_code }} {% if not property.required %} if isinstance(_{{ property.python_name }}, Unset): {{ property.python_name }} = UNSET diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index f4fd19f34..45f350f5e 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -1,10 +1,10 @@ {% macro construct(property, source) %} -def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_string() }}: - {% if "None" in property.get_type_strings_in_union(json=True) %} +def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_string() | as_unembedded_code }}: + {% if PythonCode("None") in property.get_type_strings_in_union(json=True) %} if data is None: return data {% endif %} - {% if "Unset" in property.get_type_strings_in_union(json=True) %} + {% if PythonCode("Unset") in property.get_type_strings_in_union(json=True) %} if isinstance(data, Unset): return data {% endif %} @@ -33,7 +33,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {% endif %} {% endfor %} {% if ns.contains_unmodified_properties %} - return cast({{ property.get_type_string() }}, data) + return cast({{ property.get_type_string() | as_unembedded_code }}, data) {% endif %} {{ property.python_name }} = _parse_{{ property.python_name }}({{ source }}) @@ -41,7 +41,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set ns = namespace(contains_properties_without_transform = false, contains_modified_properties = not property.required, has_if = false) %} -{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True) }}{% endif %} +{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True) | as_unembedded_code }}{% endif %} {% if not property.required and not skip_unset %} if isinstance({{ source }}, Unset): @@ -57,10 +57,10 @@ if isinstance({{ source }}, Unset): {% set ns.contains_modified_properties = true %} {% endif %} {% if not ns.has_if %} -if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() | as_unembedded_code }}): {% set ns.has_if = true %} {% elif not loop.last or ns.contains_properties_without_transform %} -elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() | as_unembedded_code }}): {% else %} else: {% endif %} @@ -76,10 +76,10 @@ else: {% macro instance_check(inner_property, source) %} -{% if inner_property.get_instance_type_string() == "None" %} +{% if inner_property.get_instance_type_string() == PythonCode("None") %} if {{ source }} is None: {% else %} -if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() | as_unembedded_code }}): {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja index 828b12b7f..1f2910008 100644 --- a/openapi_python_client/templates/property_templates/uuid_property.py.jinja +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -17,7 +17,7 @@ UUID({{ source }}) {%- else %} {% if not skip_unset %} {% if declare_type %} - {% set type_annotation = property.get_type_string(json=True) %} + {% set type_annotation = property.get_type_string(json=True) | as_unembedded_code %} {{ destination }}: {{ type_annotation }} = UNSET {% else %} {{ destination }} = UNSET diff --git a/openapi_python_client/templates/pyproject_pdm.toml.jinja b/openapi_python_client/templates/pyproject_pdm.toml.jinja index 1ce8b3ac5..e3f664f08 100644 --- a/openapi_python_client/templates/pyproject_pdm.toml.jinja +++ b/openapi_python_client/templates/pyproject_pdm.toml.jinja @@ -1,7 +1,7 @@ [project] name = "{{ project_name }}" -version = "{{ package_version }}" -description = "{{ package_description }}" +version = "{{ package_version | in_double_quote_literal }}" +description = "{{ package_description | in_double_quote_literal }}" authors = [] readme = "README.md" requires-python = ">=3.11" diff --git a/openapi_python_client/templates/pyproject_poetry.toml.jinja b/openapi_python_client/templates/pyproject_poetry.toml.jinja index d06578b17..8dc24f621 100644 --- a/openapi_python_client/templates/pyproject_poetry.toml.jinja +++ b/openapi_python_client/templates/pyproject_poetry.toml.jinja @@ -1,7 +1,7 @@ [tool.poetry] name = "{{ project_name }}" -version = "{{ package_version }}" -description = "{{ package_description }}" +version = "{{ package_version | in_double_quote_literal }}" +description = "{{ package_description | in_double_quote_literal }}" authors = [] readme = "README.md" packages = [ diff --git a/openapi_python_client/templates/pyproject_uv.toml.jinja b/openapi_python_client/templates/pyproject_uv.toml.jinja index dc5861af3..c56e203d8 100644 --- a/openapi_python_client/templates/pyproject_uv.toml.jinja +++ b/openapi_python_client/templates/pyproject_uv.toml.jinja @@ -1,7 +1,7 @@ [project] name = "{{ project_name }}" -version = "{{ package_version }}" -description = "{{ package_description }}" +version = "{{ package_version | in_double_quote_literal }}" +description = "{{ package_description | in_double_quote_literal }}" authors = [] requires-python = ">=3.11" readme = "README.md" diff --git a/openapi_python_client/templates/setup.py.jinja b/openapi_python_client/templates/setup.py.jinja index 74a7d45a0..c6e355205 100644 --- a/openapi_python_client/templates/setup.py.jinja +++ b/openapi_python_client/templates/setup.py.jinja @@ -7,8 +7,8 @@ long_description = (here / "README.md").read_text(encoding="utf-8") setup( name="{{ project_name }}", - version="{{ package_version }}", - description="{{ package_description }}", + version="{{ package_version | in_double_quote_literal }}", + description="{{ package_description | in_double_quote_literal }}", long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(), diff --git a/openapi_python_client/utils.py b/openapi_python_client/utils.py deleted file mode 100644 index 15e8c9eec..000000000 --- a/openapi_python_client/utils.py +++ /dev/null @@ -1,122 +0,0 @@ -from __future__ import annotations - -import builtins -import re -from email.message import Message -from keyword import iskeyword -from typing import Any - -from .config import Config - -DELIMITERS = r"\. _-" - - -class PythonIdentifier(str): - """A snake_case string which has been validated / transformed into a valid identifier for Python""" - - def __new__(cls, value: str, prefix: str, skip_snake_case: bool = False) -> PythonIdentifier: - new_value = sanitize(value) - if not skip_snake_case: - new_value = snake_case(new_value) - new_value = fix_reserved_words(new_value) - - if not new_value.isidentifier() or value.startswith("_"): - new_value = f"{prefix}{new_value}" - return str.__new__(cls, new_value) - - def __deepcopy__(self, _: Any) -> PythonIdentifier: - return self - - -class ClassName(str): - """A PascalCase string which has been validated / transformed into a valid class name for Python""" - - def __new__(cls, value: str, prefix: str) -> ClassName: - new_value = fix_reserved_words(pascal_case(sanitize(value))) - - if not new_value.isidentifier(): - value = f"{prefix}{new_value}" - new_value = fix_reserved_words(pascal_case(sanitize(value))) - return str.__new__(cls, new_value) - - def __deepcopy__(self, _: Any) -> ClassName: - return self - - -def sanitize(value: str) -> str: - """Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter""" - return re.sub(rf"[^\w{DELIMITERS}]+", "", value) - - -def split_words(value: str) -> list[str]: - """Split a string on words and known delimiters""" - # We can't guess words if there is no capital letter - if any(c.isupper() for c in value): - value = " ".join(re.split("([A-Z]?[a-z]+)", value)) - return re.findall(rf"[^{DELIMITERS}]+", value) - - -RESERVED_WORDS = (set(dir(builtins)) | {"self", "true", "false", "datetime"}) - { - "id", -} - - -def fix_reserved_words(value: str) -> str: - """ - Using reserved Python words as identifiers in generated code causes problems, so this function renames them. - - Args: - value: The identifier to-be that should be renamed if it's a reserved word. - - Returns: - `value` suffixed with `_` if it was a reserved word. - """ - if value in RESERVED_WORDS or iskeyword(value): - return f"{value}_" - return value - - -def snake_case(value: str) -> str: - """Converts to snake_case""" - words = split_words(sanitize(value)) - return "_".join(words).lower() - - -def pascal_case(value: str) -> str: - """Converts to PascalCase""" - words = split_words(sanitize(value)) - capitalized_words = (word.capitalize() if not word.isupper() else word for word in words) - return "".join(capitalized_words) - - -def kebab_case(value: str) -> str: - """Converts to kebab-case""" - words = split_words(sanitize(value)) - return "-".join(words).lower() - - -def remove_string_escapes(value: str) -> str: - """Used when parsing string-literal defaults to prevent escaping the string to write arbitrary Python - - **REMOVING OR CHANGING THE USAGE OF THIS FUNCTION HAS SECURITY IMPLICATIONS** - - See Also: - - https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-9x4c-63pf-525f - """ - return value.replace('"', r"\"") - - -def get_content_type(content_type: str, config: Config) -> str | None: - """ - Given a string representing a content type with optional parameters, returns the content type only - """ - content_type = config.content_type_overrides.get(content_type, content_type) - message = Message() - message.add_header("Content-Type", content_type) - - parsed_content_type = message.get_content_type() - if not content_type.startswith(parsed_content_type): - # Always defaults to `text/plain` if it's not recognized. We want to return an error, not default. - return None - - return parsed_content_type diff --git a/pdm.lock b/pdm.lock index a3e3f3f17..4311788d7 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:d12cbaca943c462ff9d65a03ded89356166f0506764a1068b836f7737a04d25e" +content_hash = "sha256:175e4c79af4623cfe2ff416ec1f952a29b9e4dc783133f62fa873455512ea5ca" [[metadata.targets]] requires_python = "~=3.11" @@ -347,6 +347,93 @@ files = [ {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] +[[package]] +name = "hypothesis" +version = "6.165.10" +requires_python = ">=3.10" +summary = "The property-based testing library for Python" +groups = ["dev"] +dependencies = [ + "exceptiongroup>=1.0.0; python_full_version < \"3.11\"", + "sortedcontainers<3.0.0,>=2.1.0", +] +files = [ + {file = "hypothesis-6.165.10-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f"}, + {file = "hypothesis-6.165.10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd"}, + {file = "hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5"}, + {file = "hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1"}, + {file = "hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52"}, + {file = "hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f"}, + {file = "hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb"}, + {file = "hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb"}, + {file = "hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e"}, + {file = "hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef"}, + {file = "hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143"}, + {file = "hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c"}, + {file = "hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7"}, + {file = "hypothesis-6.165.10-cp310-abi3-win32.whl", hash = "sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746"}, + {file = "hypothesis-6.165.10-cp310-abi3-win_amd64.whl", hash = "sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2"}, + {file = "hypothesis-6.165.10-cp310-abi3-win_arm64.whl", hash = "sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1"}, + {file = "hypothesis-6.165.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1"}, + {file = "hypothesis-6.165.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5"}, + {file = "hypothesis-6.165.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d"}, + {file = "hypothesis-6.165.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589"}, + {file = "hypothesis-6.165.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1"}, + {file = "hypothesis-6.165.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2"}, + {file = "hypothesis-6.165.10-cp311-cp311-win_amd64.whl", hash = "sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e"}, + {file = "hypothesis-6.165.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94"}, + {file = "hypothesis-6.165.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45"}, + {file = "hypothesis-6.165.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f"}, + {file = "hypothesis-6.165.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28"}, + {file = "hypothesis-6.165.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81"}, + {file = "hypothesis-6.165.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f"}, + {file = "hypothesis-6.165.10-cp312-cp312-win_amd64.whl", hash = "sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9"}, + {file = "hypothesis-6.165.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad"}, + {file = "hypothesis-6.165.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4"}, + {file = "hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d"}, + {file = "hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620"}, + {file = "hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb"}, + {file = "hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96"}, + {file = "hypothesis-6.165.10-cp313-cp313-win_amd64.whl", hash = "sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747"}, + {file = "hypothesis-6.165.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209"}, + {file = "hypothesis-6.165.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611"}, + {file = "hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e"}, + {file = "hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191"}, + {file = "hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424"}, + {file = "hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889"}, + {file = "hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063"}, + {file = "hypothesis-6.165.10-cp314-cp314-win_amd64.whl", hash = "sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd"}, + {file = "hypothesis-6.165.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff"}, + {file = "hypothesis-6.165.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4"}, + {file = "hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413"}, + {file = "hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3"}, + {file = "hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647"}, + {file = "hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3"}, + {file = "hypothesis-6.165.10-cp314-cp314t-win_amd64.whl", hash = "sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-win32.whl", hash = "sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d"}, + {file = "hypothesis-6.165.10-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015"}, + {file = "hypothesis-6.165.10-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802"}, + {file = "hypothesis-6.165.10-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0"}, + {file = "hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9"}, + {file = "hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4"}, + {file = "hypothesis-6.165.10-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757"}, + {file = "hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32"}, +] + [[package]] name = "idna" version = "3.18" @@ -908,6 +995,16 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["dev"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "syrupy" version = "5.3.2" diff --git a/pdm.minimal.lock b/pdm.minimal.lock index 2c4cbc063..ee5d2ee21 100644 --- a/pdm.minimal.lock +++ b/pdm.minimal.lock @@ -5,28 +5,25 @@ groups = ["default", "dev"] strategy = ["direct_minimal_versions", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:5e4ec1b62724a3abfac73ca1f934560c05c04dd62f46592757671ed4fc3c1d3b" +content_hash = "sha256:175e4c79af4623cfe2ff416ec1f952a29b9e4dc783133f62fa873455512ea5ca" [[metadata.targets]] requires_python = "~=3.11" [[package]] name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" +version = "0.8.0" +requires_python = ">=3.10" summary = "Reusable constraint types to use with typing.Annotated" groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.2" requires_python = ">=3.10" summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default"] @@ -36,8 +33,8 @@ dependencies = [ "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, - {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, ] [[package]] @@ -45,7 +42,7 @@ name = "attrs" version = "22.2.0" requires_python = ">=3.6" summary = "Classes Without Boilerplate" -groups = ["default"] +groups = ["default", "dev"] files = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, @@ -53,18 +50,18 @@ files = [ [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.7.22" requires_python = ">=3.7" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default"] files = [ - {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"}, - {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" requires_python = ">=3.10" summary = "Composable command line interface toolkit" groups = ["default"] @@ -72,8 +69,8 @@ dependencies = [ "colorama; platform_system == \"Windows\"", ] files = [ - {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, - {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, ] [[package]] @@ -90,103 +87,118 @@ files = [ [[package]] name = "coverage" -version = "7.14.1" +version = "7.15.4" requires_python = ">=3.10" summary = "Code coverage measurement for Python" groups = ["dev"] files = [ - {file = "coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f"}, - {file = "coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860"}, - {file = "coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df"}, - {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9"}, - {file = "coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548"}, - {file = "coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e"}, - {file = "coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3"}, - {file = "coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c"}, - {file = "coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad"}, - {file = "coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02"}, - {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a"}, - {file = "coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1"}, - {file = "coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e"}, - {file = "coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a"}, - {file = "coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793"}, - {file = "coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be"}, - {file = "coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d"}, - {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33"}, - {file = "coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c"}, - {file = "coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416"}, - {file = "coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42"}, - {file = "coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d"}, - {file = "coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2"}, - {file = "coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69"}, - {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54"}, - {file = "coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1"}, - {file = "coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce"}, - {file = "coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1"}, - {file = "coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee"}, - {file = "coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851"}, - {file = "coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4"}, - {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d"}, - {file = "coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee"}, - {file = "coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7"}, - {file = "coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343"}, - {file = "coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1"}, - {file = "coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65"}, - {file = "coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890"}, - {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd"}, - {file = "coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e"}, - {file = "coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c"}, - {file = "coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af"}, - {file = "coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2"}, - {file = "coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7"}, + {file = "coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e"}, + {file = "coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc"}, + {file = "coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4"}, + {file = "coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b"}, + {file = "coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd"}, + {file = "coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff"}, + {file = "coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c"}, + {file = "coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4"}, + {file = "coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5"}, + {file = "coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7"}, + {file = "coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425"}, + {file = "coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839"}, + {file = "coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85"}, + {file = "coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e"}, + {file = "coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753"}, + {file = "coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2"}, + {file = "coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809"}, + {file = "coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc"}, + {file = "coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a"}, + {file = "coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b"}, + {file = "coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278"}, + {file = "coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84"}, + {file = "coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00"}, ] [[package]] @@ -248,15 +260,31 @@ files = [ {file = "httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"}, ] +[[package]] +name = "hypothesis" +version = "6.100.0" +requires_python = ">=3.8" +summary = "A library for property-based testing" +groups = ["dev"] +dependencies = [ + "attrs>=22.2.0", + "exceptiongroup>=1.0.0; python_version < \"3.11\"", + "sortedcontainers<3.0.0,>=2.1.0", +] +files = [ + {file = "hypothesis-6.100.0-py3-none-any.whl", hash = "sha256:ceaeb7c051085dbec37f2fc4dca524b6304472ff1887fed53b3d84705507c10e"}, + {file = "hypothesis-6.100.0.tar.gz", hash = "sha256:1841f6b5083844cd4b66965e44a17c0dc8fe8e9c6370c1f7b8d50647fcb2efd3"}, +] + [[package]] name = "idna" -version = "3.17" +version = "3.19" requires_python = ">=3.9" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["default"] files = [ - {file = "idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c"}, - {file = "idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f"}, + {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, + {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, ] [[package]] @@ -429,13 +457,13 @@ files = [ [[package]] name = "packaging" -version = "26.2" -requires_python = ">=3.8" +version = "26.3" +requires_python = ">=3.9" summary = "Core utilities for Python packages" groups = ["dev"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -533,13 +561,13 @@ files = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" requires_python = ">=3.9" summary = "Pygments is a syntax highlighting package written in Python." groups = ["default"] files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [[package]] @@ -766,6 +794,16 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["dev"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "syrupy" version = "4.6.1" @@ -819,11 +857,11 @@ files = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" requires_python = ">=3.9" summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] diff --git a/pyproject.toml b/pyproject.toml index 0ab78d698..fa2f44a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,19 @@ repository = "https://github.com/openapi-generators/openapi-python-client" [project.scripts] openapi-python-client = "openapi_python_client.cli:app" +[dependency-groups] +dev = [ + "pytest>8", + "pytest-mock>3", + "mypy>=1.13", + "pytest-cov", + "python-multipart", + "types-PyYAML<7.0.0,>=6.0.3", + "types-certifi<2021.10.9,>=2020.0.0", + "syrupy>=4", + "hypothesis>=6.100", +] + [tool.ruff] line-length = 120 exclude = [ @@ -49,7 +62,8 @@ exclude = [ ".mypy_cache", ".venv", "openapi_python_client/templates/*", - "end_to_end_tests/*", + "end_to_end_tests/golden-records/*", + "end_to_end_tests/__snapshots__/*", "tests/test_templates/*", ] @@ -81,18 +95,6 @@ ignore_missing_imports = true [tool.pytest.ini_options] junit_family = "xunit2" -[tool.pdm.dev-dependencies] -dev = [ - "pytest>8", - "pytest-mock>3", - "mypy>=1.13", - "pytest-cov", - "python-multipart", - "types-PyYAML<7.0.0,>=6.0.3", - "types-certifi<2021.10.9,>=2020.0.0", - "syrupy>=4", -] - [tool.pdm.build] includes = [ "openapi_python_client", @@ -103,13 +105,14 @@ includes = [ [tool.pdm.scripts] lint = "ruff check ." format = "ruff format ." -mypy = "mypy openapi_python_client" +mypy = "mypy openapi_python_client fuzz" check = { composite = ["lint", "format", "mypy", "test"] } -regen = {composite = ["regen_e2e", "regen_integration"]} +regen = { composite = ["regen_e2e", "regen_integration"] } e2e = "pytest openapi_python_client end_to_end_tests/test_end_to_end.py" -re = {composite = ["regen_e2e", "e2e --snapshot-update"]} +re = { composite = ["regen_e2e", "e2e --snapshot-update"] } regen_e2e = "python -m end_to_end_tests.regen_golden_record" unit_test = "pytest tests" +fuzz = "pytest fuzz --hypothesis-show-statistics" [tool.pdm.scripts.test] cmd = "pytest tests end_to_end_tests/test_end_to_end.py end_to_end_tests/functional_tests --basetemp=tests/tmp" diff --git a/tests/conftest.py b/tests/conftest.py index 500ebf911..97d59898b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ import pytest from mypy.semanal_shared import Protocol -from openapi_python_client import Config, MetaType, utils +from openapi_python_client import Config, MetaType, strings from openapi_python_client import schema as oai from openapi_python_client.config import ConfigFile from openapi_python_client.parser.properties import ( @@ -30,7 +30,8 @@ from openapi_python_client.parser.properties.protocol import PropertyType, Value from openapi_python_client.schema.openapi_schema_pydantic import Parameter from openapi_python_client.schema.parameter_location import ParameterLocation -from openapi_python_client.utils import ClassName, PythonIdentifier +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName, PythonIdentifier @pytest.fixture(scope="session") @@ -62,7 +63,9 @@ def _factory(**kwargs): kwargs = _common_kwargs(kwargs) kwargs = { "description": "", - "class_info": Class(name=ClassName("MyClass", ""), module_name=PythonIdentifier("my_module", "")), + "class_info": Class( + name=ClassName(UntrustedString("MyClass"), ""), module_name=PythonIdentifier("my_module", "") + ), "data": oai.Schema.model_construct(), "roots": set(), "required_properties": None, @@ -134,7 +137,7 @@ def enum_property_factory() -> EnumFactory[EnumProperty]: return _simple_factory( EnumProperty, lambda kwargs: { - "class_info": Class(name=kwargs["name"], module_name=kwargs["name"]), + "class_info": Class(name=ClassName(kwargs["name"], prefix=""), module_name=kwargs["name"]), "values": {}, "value_type": str, }, @@ -152,7 +155,7 @@ def literal_enum_property_factory() -> EnumFactory[LiteralEnumProperty]: return _simple_factory( LiteralEnumProperty, lambda kwargs: { - "class_info": Class(name=kwargs["name"], module_name=kwargs["name"]), + "class_info": Class(name=UntrustedString(kwargs["name"]), module_name=kwargs["name"]), "values": set(), "value_type": str, }, @@ -321,6 +324,7 @@ def _common_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]: "example": None, **kwargs, } + kwargs["name"] = UntrustedString(kwargs["name"]) if not kwargs.get("python_name"): - kwargs["python_name"] = utils.PythonIdentifier(value=kwargs["name"], prefix="") + kwargs["python_name"] = strings.PythonIdentifier(value=kwargs["name"], prefix="") return kwargs diff --git a/tests/test___init__.py b/tests/test___init__.py index 34ad3188f..5c2bd8678 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -1,15 +1,27 @@ -from unittest.mock import MagicMock +from pathlib import Path import pytest -from openapi_python_client import Config, ErrorLevel, Project +from openapi_python_client import Config, ErrorLevel, GeneratorData, MetaType, Project from openapi_python_client.config import ConfigFile +from openapi_python_client.schema.untrusted_string import UntrustedString default_http_timeout = ConfigFile.model_json_schema()["properties"]["http_timeout"]["default"] def make_project(config: Config) -> Project: - return Project(openapi=MagicMock(title="My Test API"), config=config) + return Project( + openapi=GeneratorData( + title=UntrustedString("My API Client"), + description=None, + models=[], + version="", + errors=[], + endpoint_collections_by_tag={}, + enums=[], + ), + config=config, + ) @pytest.fixture @@ -27,9 +39,6 @@ class TestProject: def test__run_post_hooks_reports_missing_commands(self, project_with_dir: Project) -> None: fake_command_name = "blahblahdoesntexist" project_with_dir.config.post_hooks = [fake_command_name] - need_to_make_cwd = not project_with_dir.project_dir.exists() - if need_to_make_cwd: - project_with_dir.project_dir.mkdir() project_with_dir._run_post_hooks() @@ -60,3 +69,22 @@ def test__run_post_hooks_reports_stderr_of_commands_that_error(self, project_wit assert error.level == ErrorLevel.ERROR assert error.header == "python3 failed" assert "some exception" in error.detail + + def test_build_creates_parent_directories(self, tmp_path): + fresh_config = Config.from_sources( + ConfigFile(), + MetaType.POETRY, + document_source=Path("openapi.yaml"), + file_encoding="utf-8", + overwrite=False, + output_path=None, + ) + project = make_project(fresh_config) + project.project_dir = tmp_path / "nested" / "dir" / "my-api-client" + project.package_dir = project.project_dir / project.package_name + + errors = project.build() + + assert errors == [] + assert project.project_dir.exists() + assert project.project_dir.is_dir() diff --git a/tests/test_config.py b/tests/test_config.py index be2e8bf59..36cd55ca0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,7 +7,7 @@ import pytest from ruamel.yaml import YAML as _YAML -from openapi_python_client.config import ConfigFile +from openapi_python_client.config import ClassOverride, ConfigFile class YAML(_YAML): @@ -54,8 +54,8 @@ def test_load_from_path(tmp_path: Path, filename, dump, relative) -> None: config = ConfigFile.load_from_path(yml_file) assert config.field_prefix == "blah" - assert config.class_overrides["Class1"].model_dump() == override1 - assert config.class_overrides["Class2"].model_dump() == override2 + assert config.class_overrides["Class1"] == ClassOverride.model_validate(override1) + assert config.class_overrides["Class2"] == ClassOverride.model_validate(override2) assert config.project_name_override == "project-name" assert config.package_name_override == "package_name" assert config.package_version_override == "package_version" diff --git a/tests/test_parser/test_bodies.py b/tests/test_parser/test_bodies.py index 0956d11f6..a18eddb04 100644 --- a/tests/test_parser/test_bodies.py +++ b/tests/test_parser/test_bodies.py @@ -1,7 +1,8 @@ from openapi_python_client import schema as oai -from openapi_python_client.parser.bodies import body_from_data +from openapi_python_client.parser.bodies import Body, body_from_data from openapi_python_client.parser.errors import ParseError from openapi_python_client.parser.properties import Schemas +from openapi_python_client.schema import UntrustedString def test_errors(config): @@ -38,3 +39,27 @@ def test_errors(config): assert len(errs) == len(operation.request_body.content) assert all(isinstance(err, ParseError) for err in errs) + + +def test_content_type_with_special_characters(config): + """Content types containing quotes or other special characters must stay wrapped for safe template rendering""" + operation = oai.Operation( + requestBody=oai.RequestBody( + content={ + 'application/json; profile="https://example.com"': oai.MediaType( + media_type_schema=oai.Schema(type=oai.DataType.STRING) + ) + } + ), + responses={}, + ) + + bodies, _ = body_from_data( + data=operation, schemas=Schemas(), config=config, endpoint_name="endpoint", request_bodies={} + ) + + assert len(bodies) == 1 + body = bodies[0] + assert isinstance(body, Body) + assert isinstance(body.content_type, UntrustedString) + assert body.content_type == 'application/json; profile="https://example.com"' diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 3bd743804..535a04ffa 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -8,6 +8,9 @@ from openapi_python_client.parser.openapi import Endpoint, EndpointCollection, import_string_from_class from openapi_python_client.parser.properties import Class, IntProperty, Parameters, Schemas from openapi_python_client.schema import DataType +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import PythonCode, PythonIdentifier MODULE_NAME = "openapi_python_client.parser.openapi" @@ -15,13 +18,14 @@ class TestEndpoint: def make_endpoint(self): return Endpoint( - path="path", + path=UntrustedString("path"), method="method", - description=None, - name="name", + description=UntrustedString(""), + name=PythonIdentifier("name", prefix=""), requires_security=False, - tags=["tag"], + tags=[PythonIdentifier("tag", prefix="")], relative_imports={"import_3"}, + summary=UntrustedString("summary"), ) def test_add_parameters_handles_no_params(self): @@ -39,34 +43,6 @@ def test_add_parameters_handles_no_params(self): config=config, ) == (endpoint, schemas, parameters) - def test_add_parameters_parse_error(self, mocker): - endpoint = self.make_endpoint() - initial_schemas = mocker.MagicMock() - initial_parameters = mocker.MagicMock() - parse_error = ParseError(data=mocker.MagicMock()) - property_schemas = mocker.MagicMock() - mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(parse_error, property_schemas)) - param = oai.Parameter.model_construct( - name="test", required=True, param_schema=mocker.MagicMock(), param_in="cookie" - ) - config = MagicMock() - - result, schemas, parameters = Endpoint.add_parameters( - endpoint=endpoint, - data=oai.Operation.model_construct(parameters=[param]), - schemas=initial_schemas, - parameters=initial_parameters, - config=config, - ) - assert (result, schemas, parameters) == ( - ParseError( - data=parse_error.data, - detail=f"cannot parse parameter of endpoint {endpoint.name}: {parse_error.detail}", - ), - initial_schemas, - initial_parameters, - ) - @pytest.mark.parametrize( "data_type, allowed", [ @@ -83,7 +59,10 @@ def test_add_parameters_header_types(self, data_type, allowed, config): initial_schemas = Schemas() parameters = Parameters() param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema(type=data_type), param_in=oai.ParameterLocation.HEADER + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema(type=data_type), + param_in=oai.ParameterLocation.HEADER, ) result = Endpoint.add_parameters( @@ -101,7 +80,7 @@ def test_add_parameters_header_types(self, data_type, allowed, config): def test__add_parameters_parse_error_on_non_required_path_param(self, config): endpoint = self.make_endpoint() param = oai.Parameter.model_construct( - name="test", + name=UntrustedString("test"), required=False, param_schema=oai.Schema.model_construct(type="string"), param_in=oai.ParameterLocation.PATH, @@ -129,7 +108,7 @@ def test__add_parameters_handles_invalid_references(self, config): endpoint = self.make_endpoint() data = oai.Operation.model_construct( parameters=[ - oai.Reference.model_construct(ref="blah"), + oai.Reference.model_construct(ref=Ref("blah")), ] ) @@ -141,29 +120,6 @@ def test__add_parameters_handles_invalid_references(self, config): assert isinstance(error, ParseError) assert parameters == return_parameters - def test__add_parameters_resolves_references(self, mocker, param_factory, config): - """References are not supported as direct params yet""" - endpoint = self.make_endpoint() - data = oai.Operation.model_construct( - parameters=[ - oai.Reference.model_construct(ref="#components/parameters/blah"), - ] - ) - - parameters = mocker.MagicMock() - new_param = param_factory(name="blah", schema=oai.Schema.model_construct(type="string")) - parameters.classes_by_name = { - "blah": new_param, - } - parameters.classes_by_reference = {"components/parameters/blah": new_param} - - (endpoint, _, return_parameters) = endpoint.add_parameters( - endpoint=endpoint, data=data, schemas=Schemas(), parameters=parameters, config=config - ) - - assert isinstance(endpoint, Endpoint) - assert parameters == return_parameters - def test__add_parameters_skips_params_without_schemas(self, config): """Params without schemas are allowed per spec, but the any type doesn't make sense as a parameter""" endpoint = self.make_endpoint() @@ -188,19 +144,19 @@ def test__add_parameters_same_identifier_conflict(self, config): data = oai.Operation.model_construct( parameters=[ oai.Parameter.model_construct( - name="param", + name=UntrustedString("param"), param_in="path", param_schema=oai.Schema.model_construct(type="string"), required=True, ), oai.Parameter.model_construct( - name="param_path", + name=UntrustedString("param_path"), param_in="path", param_schema=oai.Schema.model_construct(type="string"), required=True, ), oai.Parameter.model_construct( - name="param", + name=UntrustedString("param"), param_in="query", param_schema=oai.Schema.model_construct(type="string"), ), @@ -219,13 +175,13 @@ def test__add_parameters_query_optionality(self, config): data = oai.Operation.model_construct( parameters=[ oai.Parameter.model_construct( - name="not_required", + name=UntrustedString("not_required"), required=False, param_schema=oai.Schema.model_construct(type="string"), param_in="query", ), oai.Parameter.model_construct( - name="required", + name=UntrustedString("required"), required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="query", @@ -247,7 +203,10 @@ def test__add_parameters_query_optionality(self, config): def test_add_parameters_duplicate_properties(self, config): endpoint = self.make_endpoint() param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="path" + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema.model_construct(type="string"), + param_in="path", ) data = oai.Operation.model_construct(parameters=[param, param]) schemas = Schemas() @@ -270,10 +229,16 @@ def test_add_parameters_duplicate_properties(self, config): def test_add_parameters_duplicate_properties_different_location(self, config): endpoint = self.make_endpoint() path_param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="path" + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema.model_construct(type="string"), + param_in="path", ) query_param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="query" + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema.model_construct(type="string"), + param_in="query", ) schemas = Schemas() parameters = Parameters() @@ -291,7 +256,7 @@ def test_add_parameters_duplicate_properties_different_location(self, config): def test_sort_parameters(self, string_property_factory): endpoint = self.make_endpoint() - endpoint.path = "/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}" + endpoint.path = UntrustedString("/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}") for i in range(1, 5): prop = string_property_factory(name=f"param{i}") @@ -305,7 +270,7 @@ def test_sort_parameters(self, string_property_factory): def test_sort_parameters_missing_param(self, string_property_factory): endpoint = self.make_endpoint() - endpoint.path = "/multiple-path-parameters/{param1}/{param2}" + endpoint.path = UntrustedString("/multiple-path-parameters/{param1}/{param2}") param = string_property_factory(name="param1") endpoint.path_parameters.append(param) @@ -313,11 +278,11 @@ def test_sort_parameters_missing_param(self, string_property_factory): assert isinstance(result, ParseError) assert "Incorrect path templating" in result.detail - assert endpoint.path in result.detail + assert endpoint.path.get_untrusted_value() in result.detail def test_sort_parameters_extra_param(self, string_property_factory): endpoint = self.make_endpoint() - endpoint.path = "/multiple-path-parameters" + endpoint.path = UntrustedString("/multiple-path-parameters") param = string_property_factory(name="param1") endpoint.path_parameters.append(param) @@ -325,235 +290,7 @@ def test_sort_parameters_extra_param(self, string_property_factory): assert isinstance(result, ParseError) assert "Incorrect path templating" in result.detail - assert endpoint.path in result.detail - - def test_from_data_bad_params(self, mocker, config): - path = mocker.MagicMock() - method = mocker.MagicMock() - parse_error = ParseError(data=mocker.MagicMock()) - return_schemas = mocker.MagicMock() - return_parameters = mocker.MagicMock() - mocker.patch.object(Endpoint, "add_parameters", return_value=(parse_error, return_schemas, return_parameters)) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - initial_schemas = mocker.MagicMock() - parameters = Parameters() - - result = Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=initial_schemas, - responses={}, - parameters=parameters, - config=config, - request_bodies={}, - ) - - assert result == (parse_error, return_schemas, return_parameters) - - def test_from_data_bad_responses(self, mocker, config): - path = mocker.MagicMock() - method = mocker.MagicMock() - parse_error = ParseError(data=mocker.MagicMock()) - param_schemas = mocker.MagicMock() - return_parameters = mocker.MagicMock() - mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), param_schemas, return_parameters) - ) - response_schemas = mocker.MagicMock() - _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=(parse_error, response_schemas)) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - initial_schemas = mocker.MagicMock() - initial_parameters = mocker.MagicMock() - - result = Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=initial_schemas, - responses={}, - parameters=initial_parameters, - config=config, - request_bodies={}, - ) - - assert result == (parse_error, response_schemas, return_parameters) - - def test_from_data_standard(self, mocker, config): - path = mocker.MagicMock() - method = mocker.MagicMock() - param_schemas = mocker.MagicMock() - param_endpoint = mocker.MagicMock() - return_parameters = mocker.MagicMock() - add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(param_endpoint, param_schemas, return_parameters) - ) - response_schemas = mocker.MagicMock() - response_endpoint = mocker.MagicMock() - _add_responses = mocker.patch.object( - Endpoint, "_add_responses", return_value=(response_endpoint, response_schemas) - ) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - initial_schemas = mocker.MagicMock() - initial_parameters = mocker.MagicMock() - - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - - Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=initial_schemas, - responses={}, - parameters=initial_parameters, - config=config, - request_bodies={}, - ) - - add_parameters.assert_called_once_with( - endpoint=Endpoint( - path=path, - method=method, - description=data.description, - summary="", - name=data.operationId, - requires_security=True, - tags=["default"], - ), - data=data, - schemas=initial_schemas, - parameters=initial_parameters, - config=config, - ) - _add_responses.assert_called_once_with( - endpoint=param_endpoint, data=data.responses, schemas=param_schemas, responses={}, config=config - ) - - def test_from_data_no_operation_id(self, mocker, config): - path = "/path/with/{param}/" - method = "get" - add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock()) - ) - _add_responses = mocker.patch.object( - Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) - ) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=None, - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - schemas = mocker.MagicMock() - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - parameters = mocker.MagicMock() - - Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=schemas, - responses={}, - parameters=parameters, - config=config, - request_bodies={}, - ) - - add_parameters.assert_called_once_with( - endpoint=Endpoint( - path=path, - method=method, - description=data.description, - summary="", - name="get_path_with_param", - requires_security=True, - tags=["default"], - ), - data=data, - schemas=schemas, - config=config, - parameters=parameters, - ) - _add_responses.assert_called_once_with( - endpoint=add_parameters.return_value[0], - data=data.responses, - schemas=add_parameters.return_value[1], - responses={}, - config=config, - ) - - def test_from_data_no_security(self, mocker, config): - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security=None, - responses=mocker.MagicMock(), - ) - add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock()) - ) - _add_responses = mocker.patch.object( - Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) - ) - path = mocker.MagicMock() - method = mocker.MagicMock() - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - schemas = mocker.MagicMock() - parameters = mocker.MagicMock() - - Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["a"], - schemas=schemas, - responses={}, - parameters=parameters, - config=config, - request_bodies={}, - ) - - add_parameters.assert_called_once_with( - endpoint=Endpoint( - path=path, - method=method, - description=data.description, - summary="", - name=data.operationId, - requires_security=False, - tags=["a"], - ), - data=data, - parameters=parameters, - schemas=schemas, - config=config, - ) - _add_responses.assert_called_once_with( - endpoint=add_parameters.return_value[0], - data=data.responses, - schemas=add_parameters.return_value[1], - responses={}, - config=config, - ) + assert endpoint.path.get_untrusted_value() in result.detail def test_from_data_some_bad_bodies(self, config): endpoint, _, _ = Endpoint.from_data( @@ -571,7 +308,7 @@ def test_from_data_some_bad_bodies(self, config): config=config, parameters=Parameters(), tags=["tag"], - path="/", + path=UntrustedString("/"), method="get", request_bodies={}, ) @@ -595,7 +332,7 @@ def test_from_data_all_bodies_bad(self, config): config=config, parameters=Parameters(), tags=["tag"], - path="/", + path=UntrustedString("/"), method="get", request_bodies={}, ) @@ -610,10 +347,10 @@ def test_response_type(self, response_types, expected): endpoint = self.make_endpoint() for response_type in response_types: mock_response = MagicMock() - mock_response.prop.get_type_string.return_value = response_type + mock_response.prop.get_type_string.return_value = PythonCode(response_type) endpoint.responses.patterns.append(mock_response) - assert endpoint.response_type() == expected + assert endpoint.response_type() == PythonCode(expected) class TestImportStringFromReference: @@ -634,19 +371,23 @@ def test_import_string_from_reference_with_prefix(self, mocker): class TestEndpointCollection: def test_from_data_overrides_path_item_params_with_operation_params(self, config): data = { - "/": oai.PathItem.model_construct( + UntrustedString("/"): oai.PathItem.model_construct( parameters=[ oai.Parameter.model_construct( - name="param", param_in="query", param_schema=oai.Schema.model_construct(type="string") + name=UntrustedString("param"), + param_in="query", + param_schema=oai.Schema.model_construct(type="string"), ), ], get=oai.Operation.model_construct( parameters=[ oai.Parameter.model_construct( - name="param", param_in="query", param_schema=oai.Schema.model_construct(type="integer") + name=UntrustedString("param"), + param_in="query", + param_schema=oai.Schema.model_construct(type="integer"), ) ], - responses={"200": oai.Response.model_construct(description="blah")}, + responses={UntrustedString("200"): oai.Response.model_construct(description="blah")}, ), ) } diff --git a/tests/test_parser/test_properties/test_enum_property.py b/tests/test_parser/test_properties/test_enum_property.py new file mode 100644 index 000000000..afa9dc843 --- /dev/null +++ b/tests/test_parser/test_properties/test_enum_property.py @@ -0,0 +1,27 @@ +from openapi_python_client.parser.properties import EnumProperty +from openapi_python_client.parser.properties.schemas import Class +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName, PythonIdentifier + + +def _class_info() -> Class: + return Class(name=ClassName(UntrustedString("MyEnum"), ""), module_name=PythonIdentifier("my_enum", "")) + + +def test_values_from_list_escapes_or_strips_control_characters(): + """Values are embedded in double-quoted string literals; newlines and control characters must be escaped""" + values = EnumProperty.values_from_list(["line\nbreak", "tab\there", "nul\x00byte"], _class_info(), []) + + assert values["LINEBREAK"] == "linebreak" + assert values["TABHERE"] == "tab\\there" + assert values["NULBYTE"] == "nulbyte" + # Every value must be safe inside a "..." literal in generated source + for value in values.values(): + assert all(ord(c) >= 0x20 for c in value), value + + +def test_values_from_list_sanitizes_keys_with_backslash(): + """A backslash in a value must not survive into the generated identifier""" + values = EnumProperty.values_from_list(["one\\two"], _class_info(), []) + + assert list(values) == ["ONE_TWO"] diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 512f5cdac..09446414f 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -1,24 +1,18 @@ -from unittest.mock import call - import pytest import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import ParameterError, PropertyError +from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import ( Class, - Parameters, ReferencePath, Schemas, - _create_schemas, - _process_model_errors, - _process_models, _propogate_removal, - build_parameters, build_schemas, property_from_data, ) -from openapi_python_client.schema import Parameter, Reference, Schema -from openapi_python_client.utils import ClassName, PythonIdentifier +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName, PythonCode, PythonIdentifier MODULE_NAME = "openapi_python_client.parser.properties" @@ -78,44 +72,44 @@ def test_get_type_string( inner_properties=[date_time_property_factory(), string_property_factory()], ) - assert p.get_base_type_string() == "datetime.datetime | str" + assert p.get_base_type_string() == PythonCode("datetime.datetime | str") - assert p.get_type_string(no_optional=no_optional, json=json) == expected + assert p.get_type_string(no_optional=no_optional, json=json) == PythonCode(expected) def test_get_base_type_string_base_inners( self, union_property_factory, date_time_property_factory, string_property_factory ): p = union_property_factory(inner_properties=[date_time_property_factory(), string_property_factory()]) - assert p.get_base_type_string() == "datetime.datetime | str" + assert p.get_base_type_string() == PythonCode("datetime.datetime | str") def test_get_base_type_string_one_base_inner(self, union_property_factory, date_time_property_factory): p = union_property_factory( inner_properties=[date_time_property_factory()], ) - assert p.get_base_type_string() == "datetime.datetime" + assert p.get_base_type_string() == PythonCode("datetime.datetime") def test_get_base_type_string_one_model_inner(self, union_property_factory, model_property_factory): p = union_property_factory( inner_properties=[model_property_factory()], ) - assert p.get_base_type_string() == "MyClass" + assert p.get_base_type_string() == PythonCode("MyClass") def test_get_base_type_string_model_inners( self, union_property_factory, date_time_property_factory, model_property_factory ): p = union_property_factory(inner_properties=[date_time_property_factory(), model_property_factory()]) - assert p.get_base_type_string() == "datetime.datetime | MyClass" + assert p.get_base_type_string() == PythonCode("datetime.datetime | MyClass") def test_get_base_json_type_string(self, union_property_factory, date_time_property_factory): p = union_property_factory( inner_properties=[date_time_property_factory()], ) - assert p.get_base_json_type_string() == "str" + assert p.get_base_json_type_string() == PythonCode("str") @pytest.mark.parametrize("required", (True, False)) def test_get_type_imports(self, union_property_factory, date_time_property_factory, required): @@ -139,15 +133,17 @@ class TestPropertyFromData: def test_property_from_data_ref_model(self, model_property_factory, config): name = "new_name" required = False - class_name = ClassName("MyModel", "") - data = oai.Reference.model_construct(ref=f"#/components/schemas/{class_name}") + class_name = ClassName(UntrustedString("MyModel"), "") + data = oai.Reference.model_construct(ref=Ref(f"#/components/schemas/{class_name}")) class_info = Class(name=class_name, module_name=PythonIdentifier("my_model", "")) existing_model = model_property_factory( name="old_name", class_info=class_info, ) - schemas = Schemas(classes_by_reference={ReferencePath(f"/components/schemas/{class_name}"): existing_model}) + schemas = Schemas( + classes_by_reference={ReferencePath(UntrustedString(f"/components/schemas/{class_name}")): existing_model} + ) prop, new_schemas = property_from_data( name=name, required=required, data=data, schemas=schemas, parent_name="", config=config @@ -178,8 +174,8 @@ def test_property_from_data_ref_not_found(self, mocker): def test_property_from_data_ref(self, any_property_factory, references_exist, config): name = "new_name" required = False - ref_path = "/components/schemas/RefName" - data = oai.Reference.model_construct(ref=f"#{ref_path}") + ref_path = ReferencePath(UntrustedString("/components/schemas/RefName")) + data = oai.Reference.model_construct(ref=Ref(f"#{ref_path.get_untrusted_value()}")) roots = {"new_root"} existing_property = any_property_factory(name="old_name") @@ -194,23 +190,6 @@ def test_property_from_data_ref(self, any_property_factory, references_exist, co assert schemas == new_schemas assert schemas.dependencies == {ref_path: {*roots, *references.get(ref_path, set())}} - def test_property_from_data_invalid_ref(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Reference.model_construct(ref=mocker.MagicMock()) - parse_reference_path = mocker.patch( - f"{MODULE_NAME}.parse_reference_path", return_value=PropertyError(detail="bad stuff") - ) - schemas = Schemas() - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=mocker.MagicMock() - ) - - parse_reference_path.assert_called_once_with(data.ref) - assert prop == PropertyError(data=data, detail="bad stuff") - assert schemas == new_schemas - class TestStringBasedProperty: def test__string_based_property_binary_format(self, file_property_factory, config): @@ -224,121 +203,29 @@ def test__string_based_property_binary_format(self, file_property_factory, confi assert p == file_property_factory(name=name, required=required) -class TestCreateSchemas: - def test_dereference_references(self, mocker, config): - components = {"a_ref": Reference(ref="#/components/schemas/a_schema"), "a_schema": Schema.model_construct()} - update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - schemas = Schemas() - - result = _create_schemas(components=components, schemas=schemas, config=config) - - parse_reference_path.assert_has_calls( - [call("#/components/schemas/a_ref"), call("#/components/schemas/a_schema")] - ) - update_schemas_with_data.assert_called_with( - ref_path=parse_reference_path.return_value, - config=config, - data=components["a_schema"], - schemas=result, - ) - assert result == update_schemas_with_data.return_value - - def test_records_bad_uris_and_keeps_going(self, mocker, config): - components = {"first": Schema.model_construct(), "second": Schema.model_construct()} - update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") - parse_reference_path = mocker.patch( - f"{MODULE_NAME}.parse_reference_path", side_effect=[PropertyError(detail="some details"), "a_path"] - ) - schemas = Schemas() - - result = _create_schemas(components=components, schemas=schemas, config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/schemas/first"), - call("#/components/schemas/second"), - ] - ) - update_schemas_with_data.assert_called_once_with( - ref_path="a_path", - config=config, - data=components["second"], - schemas=Schemas(errors=[PropertyError(detail="some details", data=components["first"])]), - ) - assert result == update_schemas_with_data.return_value - - def test_retries_failing_properties_while_making_progress(self, mocker, config): - components = {"first": Schema.model_construct(), "second": Schema.model_construct()} - update_schemas_with_data = mocker.patch( - f"{MODULE_NAME}.update_schemas_with_data", side_effect=[PropertyError(), Schemas(), PropertyError()] - ) - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - schemas = Schemas() - - result = _create_schemas(components=components, schemas=schemas, config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/schemas/first"), - call("#/components/schemas/second"), - call("#/components/schemas/first"), - ] - ) - assert update_schemas_with_data.call_count == 3 - assert result.errors == [PropertyError()] - - class TestProcessModels: - def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_factory, config): - class_name = ClassName("class_name", "") - recursive_model = model_property_factory( - class_info=Class(name=class_name, module_name=PythonIdentifier("module_name", "")) - ) - second_model = model_property_factory() - schemas = Schemas( - classes_by_name={ - "recursive": recursive_model, - "second": second_model, - }, - models_to_process=[recursive_model, second_model], - ) - recursion_error = PropertyError(data=Reference.model_construct(ref=f"#/{class_name}")) - process_model = mocker.patch(f"{MODULE_NAME}.process_model", side_effect=[recursion_error, schemas]) - process_model_errors = mocker.patch(f"{MODULE_NAME}._process_model_errors", return_value=["error"]) - - result = _process_models(schemas=schemas, config=config) - - process_model.assert_has_calls( - [ - call(recursive_model, schemas=schemas, config=config), - call(schemas.classes_by_name["second"], schemas=schemas, config=config), - ] - ) - assert process_model_errors.was_called_once_with([(recursive_model, recursion_error)]) - assert all(error in result.errors for error in process_model_errors.return_value) - assert "\n\nRecursive allOf reference found" in recursion_error.detail - def test_resolve_reference_to_single_allof_reference(self, config, model_property_factory): # test for https://github.com/openapi-generators/openapi-python-client/issues/1091 components = { - "Model1": oai.Schema.model_construct( + UntrustedString("Model1"): oai.Schema.model_construct( type="object", properties={ - "prop1": oai.Schema.model_construct(type="string"), + UntrustedString("prop1"): oai.Schema.model_construct(type="string"), }, ), - "Model2": oai.Schema.model_construct( + UntrustedString("Model2"): oai.Schema.model_construct( allOf=[ - oai.Reference.model_construct(ref="#/components/schemas/Model1"), + oai.Reference.model_construct(ref=Ref("#/components/schemas/Model1")), ] ), - "Model3": oai.Schema.model_construct( + UntrustedString("Model3"): oai.Schema.model_construct( allOf=[ - oai.Reference.model_construct(ref="#/components/schemas/Model2"), + oai.Reference.model_construct(ref=Ref("#/components/schemas/Model2")), oai.Schema.model_construct( type="object", properties={ - "prop2": oai.Schema.model_construct(type="string"), + UntrustedString("prop2"): oai.Schema.model_construct(type="string"), }, ), ], @@ -361,8 +248,8 @@ def test_resolve_reference_to_single_allof_reference(self, config, model_propert "/components/schemas/Model3", } assert ( - result.classes_by_reference["/components/schemas/Model2"].class_info - == result.classes_by_reference["/components/schemas/Model1"].class_info + result.classes_by_reference[ReferencePath(UntrustedString("/components/schemas/Model2"))].class_info + == result.classes_by_reference[ReferencePath(UntrustedString("/components/schemas/Model1"))].class_info ) # Verify that Model3 extended the properties from Model1 @@ -371,9 +258,9 @@ def test_resolve_reference_to_single_allof_reference(self, config, model_propert class TestPropogateRemoval: def test_propogate_removal_class_name(self): - root = ClassName("ClassName", "") - ref_path = ReferencePath("/reference") - other_class_name = ClassName("OtherClassName", "") + root = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/reference")) + other_class_name = ClassName(UntrustedString("OtherClassName"), "") schemas = Schemas( classes_by_name={root: None, other_class_name: None}, classes_by_reference={ref_path: None}, @@ -388,9 +275,9 @@ def test_propogate_removal_class_name(self): assert not error.detail def test_propogate_removal_ref_path(self): - root = ReferencePath("/root/reference") - class_name = ClassName("ClassName", "") - ref_path = ReferencePath("/ref/path") + root = ReferencePath(UntrustedString("/root/reference")) + class_name = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/ref/path")) schemas = Schemas( classes_by_name={class_name: None}, classes_by_reference={root: None, ref_path: None}, @@ -402,25 +289,28 @@ def test_propogate_removal_ref_path(self): assert schemas.classes_by_name == {} assert schemas.classes_by_reference == {} - assert error.detail == f"\n{root}\n{ref_path}" + assert error.detail == f"\n{root.get_untrusted_value()}\n{ref_path.get_untrusted_value()}" def test_propogate_removal_ref_path_no_refs(self): - root = ReferencePath("/root/reference") - class_name = ClassName("ClassName", "") - ref_path = ReferencePath("/ref/path") - schemas = Schemas(classes_by_name={class_name: None}, classes_by_reference={root: None, ref_path: None}) + root = ReferencePath(UntrustedString("/root/reference")) + class_name = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/ref/path")) + schemas = Schemas( + classes_by_name={class_name: None}, + classes_by_reference={root: None, ref_path: None}, + ) error = PropertyError() _propogate_removal(root=root, schemas=schemas, error=error) assert schemas.classes_by_name == {class_name: None} assert schemas.classes_by_reference == {ref_path: None} - assert error.detail == f"\n{root}" + assert error.detail == f"\n{root.get_untrusted_value()}" def test_propogate_removal_ref_path_already_removed(self): - root = ReferencePath("/root/reference") - class_name = ClassName("ClassName", "") - ref_path = ReferencePath("/ref/path") + root = ReferencePath(UntrustedString("/root/reference")) + class_name = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/ref/path")) schemas = Schemas( classes_by_name={class_name: None}, classes_by_reference={ref_path: None}, @@ -433,106 +323,3 @@ def test_propogate_removal_ref_path_already_removed(self): assert schemas.classes_by_name == {class_name: None} assert schemas.classes_by_reference == {ref_path: None} assert not error.detail - - -def test_process_model_errors(mocker, model_property_factory): - propogate_removal = mocker.patch(f"{MODULE_NAME}._propogate_removal") - model_errors = [ - (model_property_factory(roots={"root1", "root2"}), PropertyError(detail="existing detail")), - (model_property_factory(roots=set()), PropertyError()), - (model_property_factory(roots={"root1", "root3"}), PropertyError(detail="other existing detail")), - ] - schemas = Schemas() - - result = _process_model_errors(model_errors, schemas=schemas) - - propogate_removal.assert_has_calls( - [call(root=root, schemas=schemas, error=error) for model, error in model_errors for root in model.roots] - ) - assert result == [error for _, error in model_errors] - assert all("\n\nFailure to process schema has resulted in the removal of:" in error.detail for error in result) - - -class TestBuildParameters: - def test_skips_references_and_keeps_going(self, mocker, config): - parameters = { - "reference": Reference(ref="#/components/parameters/another_parameter"), - "defined": Parameter( - name="page", - param_in="query", - required=False, - style="form", - explode=True, - schema=oai.Schema(type="integer", default=0), - ), - } - - update_parameters_with_data = mocker.patch(f"{MODULE_NAME}.update_parameters_with_data") - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - - result = build_parameters(components=parameters, parameters=Parameters(), config=config) - # Should not even try to parse a path for the Reference - parse_reference_path.assert_called_once_with("#/components/parameters/defined") - update_parameters_with_data.assert_called_once_with( - ref_path=parse_reference_path.return_value, - data=parameters["defined"], - parameters=Parameters( - errors=[ParameterError(detail="Reference parameters are not supported.", data=parameters["reference"])] - ), - config=config, - ) - assert result == update_parameters_with_data.return_value - - def test_records_bad_uris_and_keeps_going(self, mocker, config): - parameters = {"first": Parameter.model_construct(), "second": Parameter.model_construct()} - update_parameters_with_data = mocker.patch(f"{MODULE_NAME}.update_parameters_with_data") - parse_reference_path = mocker.patch( - f"{MODULE_NAME}.parse_reference_path", side_effect=[ParameterError(detail="some details"), "a_path"] - ) - - result = build_parameters(components=parameters, parameters=Parameters(), config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/parameters/first"), - call("#/components/parameters/second"), - ] - ) - update_parameters_with_data.assert_called_once_with( - ref_path="a_path", - data=parameters["second"], - parameters=Parameters(errors=[ParameterError(detail="some details", data=parameters["first"])]), - config=config, - ) - assert result == update_parameters_with_data.return_value - - def test_retries_failing_parameters_while_making_progress(self, mocker, config): - parameters = {"first": Parameter.model_construct(), "second": Parameter.model_construct()} - update_parameters_with_data = mocker.patch( - f"{MODULE_NAME}.update_parameters_with_data", side_effect=[ParameterError(), Parameters(), ParameterError()] - ) - - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - result = build_parameters(components=parameters, parameters=Parameters(), config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/parameters/first"), - call("#/components/parameters/second"), - call("#/components/parameters/first"), - ] - ) - assert update_parameters_with_data.call_count == 3 - assert result.errors == [ParameterError()] - - -def test_build_schemas(mocker, config): - create_schemas = mocker.patch(f"{MODULE_NAME}._create_schemas") - process_models = mocker.patch(f"{MODULE_NAME}._process_models") - - components = {"a_ref": Reference.model_construct(), "a_schema": Schema.model_construct()} - schemas = Schemas() - - result = build_schemas(components=components, schemas=schemas, config=config) - - create_schemas.assert_called_once_with(components=components, schemas=schemas, config=config) - process_models.assert_called_once_with(schemas=create_schemas.return_value, config=config) - assert result == process_models.return_value diff --git a/tests/test_parser/test_properties/test_merge_properties.py b/tests/test_parser/test_properties/test_merge_properties.py index 819f9ec26..ea77e080b 100644 --- a/tests/test_parser/test_properties/test_merge_properties.py +++ b/tests/test_parser/test_properties/test_merge_properties.py @@ -10,6 +10,7 @@ from openapi_python_client.parser.properties.protocol import Value from openapi_python_client.parser.properties.schemas import Class from openapi_python_client.parser.properties.string import StringProperty +from openapi_python_client.strings import PythonCode MODULE_NAME = "openapi_python_client.parser.properties.merge_properties" @@ -23,9 +24,9 @@ def test_merge_basic_attributes_same_type( model_property_factory, ): basic_props = [ - boolean_property_factory(default=Value(python_code="True", raw_value="True")), - int_property_factory(default=Value("1", 1)), - float_property_factory(default=Value("1.5", 1.5)), + boolean_property_factory(default=Value(python_code=PythonCode("True"), raw_value="True")), + int_property_factory(default=Value(PythonCode("1"), 1)), + float_property_factory(default=Value(PythonCode("1.5"), 1.5)), string_property_factory(default=StringProperty.convert_value("x")), list_property_factory(), model_property_factory(), @@ -70,14 +71,14 @@ def test_incompatible_types( def test_merge_int_with_float(int_property_factory, float_property_factory): int_prop = int_property_factory(description="desc1") - float_prop = float_property_factory(default=Value("2", 2), description="desc2") + float_prop = float_property_factory(default=Value(PythonCode("2"), 2), description="desc2") assert merge_properties(int_prop, float_prop) == ( - evolve(int_prop, default=Value("2", 2), description=float_prop.description) + evolve(int_prop, default=Value(PythonCode("2"), 2), description=float_prop.description) ) - assert merge_properties(float_prop, int_prop) == evolve(int_prop, default=Value("2", 2)) + assert merge_properties(float_prop, int_prop) == evolve(int_prop, default=Value(PythonCode("2"), 2)) - float_prop_with_non_int_default = evolve(float_prop, default=Value("2.5", 2.5)) + float_prop_with_non_int_default = evolve(float_prop, default=Value(PythonCode("2.5"), 2.5)) error = merge_properties(int_prop, float_prop_with_non_int_default) assert isinstance(error, PropertyError), "Expected invalid default to error" assert error.detail == "Invalid int value: 2.5" @@ -93,9 +94,9 @@ def test_merge_with_any( ): original_desc = "description" props = [ - boolean_property_factory(default=Value("True", "True"), description=original_desc), - int_property_factory(default=Value("1", "1"), description=original_desc), - float_property_factory(default=Value("1.5", "1.5"), description=original_desc), + boolean_property_factory(default=Value(PythonCode("True"), "True"), description=original_desc), + int_property_factory(default=Value(PythonCode("1"), "1"), description=original_desc), + float_property_factory(default=Value(PythonCode("1.5"), "1.5"), description=original_desc), string_property_factory(default=StringProperty.convert_value("x"), description=original_desc), model_property_factory(description=original_desc), ] @@ -151,10 +152,10 @@ def test_merge_enums(literal_enums, enum_property_factory, literal_enum_property def test_merge_string_with_string_enum( literal_enums, string_property_factory, enum_property_factory, literal_enum_property_factory ): - string_prop = string_property_factory(default=Value("A", "A"), description="desc1", example="example1") + string_prop = string_property_factory(default=Value(PythonCode("A"), "A"), description="desc1", example="example1") enum_prop = ( literal_enum_property_factory( - default=Value("'B'", "B"), + default=Value(PythonCode("'B'"), "B"), description="desc2", example="example2", values={"A", "B"}, @@ -162,7 +163,7 @@ def test_merge_string_with_string_enum( ) if literal_enums else enum_property_factory( - default=Value("test.B", "B"), + default=Value(PythonCode("Test.B"), "B"), description="desc2", example="example2", values={"A": "A", "B": "B"}, @@ -174,7 +175,7 @@ def test_merge_string_with_string_enum( assert merge_properties(enum_prop, string_prop) == evolve( enum_prop, required=True, - default=Value("'A'" if literal_enums else "test.A", "A"), + default=Value(PythonCode("'A'") if literal_enums else PythonCode("Test.A"), "A"), description=string_prop.description, example=string_prop.example, ) @@ -184,10 +185,10 @@ def test_merge_string_with_string_enum( def test_merge_int_with_int_enum( literal_enums, int_property_factory, enum_property_factory, literal_enum_property_factory ): - int_prop = int_property_factory(default=Value("1", 1), description="desc1", example="example1") + int_prop = int_property_factory(default=Value(PythonCode("1"), 1), description="desc1", example="example1") enum_prop = ( literal_enum_property_factory( - default=Value("1", 1), + default=Value(PythonCode("1"), 1), description="desc2", example="example2", values={1, 2}, @@ -195,7 +196,7 @@ def test_merge_int_with_int_enum( ) if literal_enums else enum_property_factory( - default=Value("test.VALUE_1", 1), + default=Value(PythonCode("Test.VALUE_1"), 1), description="desc2", example="example2", values={"VALUE_1": 1, "VALUE_2": 2}, diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 8d686edd7..b8dc09213 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -10,6 +10,9 @@ _PropertyData, process_model, ) +from openapi_python_client.schema import DataType +from openapi_python_client.schema.ref import Ref +from openapi_python_client.strings import PythonCode MODULE_NAME = "openapi_python_client.parser.properties.model_property" @@ -30,7 +33,7 @@ def test_get_type_string(self, no_optional, required, json, expected, model_prop required=required, ) - assert prop.get_type_string(no_optional=no_optional, json=json) == expected + assert prop.get_type_string(no_optional=no_optional, json=json) == PythonCode(expected) def test_get_imports(self, model_property_factory): prop = model_property_factory(required=False) @@ -49,7 +52,7 @@ def test_get_lazy_imports(self, model_property_factory): def test_get_base_type_string(self, model_property_factory): m = model_property_factory() - assert m.get_base_type_string() == "MyClass" + assert m.get_base_type_string() == PythonCode("MyClass") class TestBuild: @@ -61,7 +64,7 @@ class TestBuild: (None, ANY_ADDITIONAL_PROPERTY), (False, None), ( - oai.Schema.model_construct(type="string"), + oai.Schema(type=DataType.STRING), StringProperty( name="AdditionalProperty", required=True, @@ -212,7 +215,7 @@ def test_model_naming( def test_model_bad_properties(self, config): data = oai.Schema( properties={ - "bad": oai.Reference.model_construct(ref="#/components/schema/NotExist"), + "bad": oai.Reference.model_construct(ref=Ref("#/components/schema/NotExist")), }, ) result = ModelProperty.build( @@ -298,7 +301,10 @@ def test_conflicting_properties_different_types( self, model_property_factory, string_property_factory, int_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) schemas = Schemas( classes_by_reference={ @@ -316,7 +322,7 @@ def test_conflicting_properties_different_types( def test_process_properties_reference_not_exist(self, config): data = oai.Schema( properties={ - "bad": oai.Reference.model_construct(ref="#/components/schema/NotExist"), + "bad": oai.Reference.model_construct(ref=Ref("#/components/schema/NotExist")), }, ) @@ -325,7 +331,9 @@ def test_process_properties_reference_not_exist(self, config): assert isinstance(result, PropertyError) def test_process_properties_all_of_reference_not_exist(self, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="#/components/schema/NotExist")]) + data = oai.Schema.model_construct( + allOf=[oai.Reference.model_construct(ref=Ref("#/components/schema/NotExist"))] + ) result = _process_properties(data=data, class_name="", schemas=Schemas(), config=config, roots={"root"}) @@ -340,7 +348,7 @@ def test_process_properties_model_property_roots(self, model_property_factory, c assert all(root in result.optional_props[0].roots for root in roots) def test_invalid_reference(self, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="ThisIsNotGood")]) + data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref=Ref("ThisIsNotGood"))]) schemas = Schemas() result = _process_properties(data=data, schemas=schemas, class_name="", config=config, roots={"root"}) @@ -348,7 +356,7 @@ def test_invalid_reference(self, config): assert isinstance(result, PropertyError) def test_non_model_reference(self, enum_property_factory, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="#/First")]) + data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref=Ref("#/First"))]) schemas = Schemas( classes_by_reference={ "/First": enum_property_factory(), @@ -360,7 +368,7 @@ def test_non_model_reference(self, enum_property_factory, config): assert isinstance(result, PropertyError) def test_reference_not_processed(self, model_property_factory, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="#/Unprocessed")]) + data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref=Ref("#/Unprocessed"))]) schemas = Schemas( classes_by_reference={ "/Unprocessed": model_property_factory(), @@ -375,7 +383,10 @@ def test_allof_string_and_string_enum( self, model_property_factory, enum_property_factory, string_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( values={"foo": "foo"}, @@ -397,7 +408,10 @@ def test_allof_string_enum_and_string( self, model_property_factory, enum_property_factory, string_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( required=False, @@ -418,7 +432,10 @@ def test_allof_string_enum_and_string( def test_allof_int_and_int_enum(self, model_property_factory, enum_property_factory, int_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( values={"foo": 1}, @@ -438,7 +455,10 @@ def test_allof_enum_incompatible_type( self, model_property_factory, enum_property_factory, int_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( values={"foo": 1}, @@ -456,7 +476,10 @@ def test_allof_enum_incompatible_type( def test_allof_string_enums(self, model_property_factory, enum_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property1 = enum_property_factory( name="an_enum", @@ -480,7 +503,10 @@ def test_allof_string_enums(self, model_property_factory, enum_property_factory, def test_allof_int_enums(self, model_property_factory, enum_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property1 = enum_property_factory( name="an_enum", @@ -504,7 +530,10 @@ def test_allof_int_enums(self, model_property_factory, enum_property_factory, co def test_allof_enums_are_not_subsets(self, model_property_factory, enum_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property1 = enum_property_factory( name="an_enum", @@ -528,7 +557,10 @@ def test_allof_enums_are_not_subsets(self, model_property_factory, enum_property def test_duplicate_properties(self, model_property_factory, string_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) prop = string_property_factory(required=False) schemas = Schemas( @@ -560,7 +592,7 @@ def test_allof_required_override(self, model_property_factory, string_property_f data = oai.Schema.model_construct( allOf=[ - oai.Reference.model_construct(ref="#/FooBase"), + oai.Reference.model_construct(ref=Ref("#/FooBase")), oai.Schema.model_construct(type="object", required=["bar"]), ] ) @@ -589,7 +621,10 @@ def test_mixed_requirements( config, ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) schemas = Schemas( classes_by_reference={ diff --git a/tests/test_parser/test_properties/test_none.py b/tests/test_parser/test_properties/test_none.py index b6289cdb8..760f6c702 100644 --- a/tests/test_parser/test_properties/test_none.py +++ b/tests/test_parser/test_properties/test_none.py @@ -1,7 +1,7 @@ from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import NoneProperty from openapi_python_client.parser.properties.protocol import Value -from openapi_python_client.utils import PythonIdentifier +from openapi_python_client.strings import PythonIdentifier def test_default(): diff --git a/tests/test_parser/test_properties/test_protocol.py b/tests/test_parser/test_properties/test_protocol.py index 625556c5b..c69aa955b 100644 --- a/tests/test_parser/test_properties/test_protocol.py +++ b/tests/test_parser/test_properties/test_protocol.py @@ -3,7 +3,9 @@ import pytest from openapi_python_client.parser.properties import AnyProperty -from openapi_python_client.parser.properties.protocol import Value +from openapi_python_client.parser.properties.protocol import Value, convert_example +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import PythonCode @pytest.mark.parametrize( @@ -23,7 +25,7 @@ def test_get_type_string(any_property_factory, mocker, required, no_optional, js mocker.patch.object(AnyProperty, "_type_string", "TestType") mocker.patch.object(AnyProperty, "_json_type_string", "str") p = any_property_factory(required=required) - assert p.get_type_string(no_optional=no_optional, json=json) == expected + assert p.get_type_string(no_optional=no_optional, json=json) == PythonCode(expected) @pytest.mark.parametrize( @@ -38,10 +40,10 @@ def test_get_type_string(any_property_factory, mocker, required, no_optional, js def test_to_string(default: str | None, required: bool, expected: str, any_property_factory): name = "test" p = any_property_factory( - name=name, required=required, default=Value(default, default) if default is not None else None + name=name, required=required, default=Value(PythonCode(default), default) if default is not None else None ) - assert p.to_string() == expected + assert p.to_string() == PythonCode(expected) def test_get_imports(any_property_factory): @@ -50,3 +52,66 @@ def test_get_imports(any_property_factory): p = any_property_factory(name="test", required=False, default=None) assert p.get_imports(prefix="") == {"from types import UNSET, Unset"} + + +def test_to_docstring_escapes_description_and_example(any_property_factory): + """Descriptions and examples must not be able to break out of the docstring""" + p = any_property_factory( + description='Break out """ print("uh oh")', + example='Example """ print("uh oh")', + ) + + doc = p.to_docstring() + + assert '"""' not in doc + assert r'\"\"\" print("uh oh")' in doc + + +def test_to_docstring_escapes_type_string(any_property_factory, mocker): + """A type string containing literal values (e.g. const) must not break out of the docstring""" + mocker.patch.object(AnyProperty, "_type_string", 'Literal[\'"""\']') + p = any_property_factory() + + doc = p.to_docstring() + + assert '"""' not in doc + + +def test_to_docstring_escapes_default(any_property_factory): + """A default's python_code must not be able to break out of the docstring""" + # python_code for the string default '"""' is the 5-char source: "\"\"\"" + p = any_property_factory(default=Value(python_code=PythonCode('"\\"\\"\\""'), raw_value='"""')) + + doc = p.to_docstring() + + # The raw triple-quote must not survive into the docstring text + assert '"""' not in doc + + +def test_get_instance_type_string(any_property_factory): + p = any_property_factory(required=False) + assert p.get_instance_type_string() == PythonCode("Any") + + +class TestConvertExample: + def test_none(self): + assert convert_example(None) is None + + def test_untrusted_string_passes_through(self): + example = UntrustedString("an example") + assert convert_example(example) is example + + def test_string_is_wrapped(self): + converted = convert_example("an example") + assert isinstance(converted, UntrustedString) + assert converted == "an example" + + def test_non_string_is_stringified(self): + """OpenAPI examples can be any type; they only appear in docstrings so stringify them""" + converted = convert_example({"a": 1}) + assert isinstance(converted, UntrustedString) + assert converted == "{'a': 1}" + + def test_property_converts_example(self, any_property_factory): + p = any_property_factory(example={"a": 1}) + assert p.example == "{'a': 1}" diff --git a/tests/test_parser/test_properties/test_schemas.py b/tests/test_parser/test_properties/test_schemas.py index 7e7af8514..8433404d2 100644 --- a/tests/test_parser/test_properties/test_schemas.py +++ b/tests/test_parser/test_properties/test_schemas.py @@ -5,12 +5,14 @@ from openapi_python_client.parser.errors import ParameterError from openapi_python_client.parser.properties import Class, Parameters from openapi_python_client.parser.properties.schemas import ( + ReferencePath, parameter_from_data, parameter_from_reference, - update_parameters_with_data, ) from openapi_python_client.schema import Parameter, ParameterLocation, Reference, Schema -from openapi_python_client.utils import ClassName +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName MODULE_NAME = "openapi_python_client.parser.properties.schemas" @@ -63,7 +65,7 @@ def test_parameters_without_schema_are_ignored(self, config): def test_registers_new_parameters(self, config): param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() + name=UntrustedString("a_param"), param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() ) parameters = Parameters() param_or_error, new_parameters = parameter_from_data( @@ -81,10 +83,10 @@ def test_returns_parameter_if_parameter_provided(self): assert param_or_error == param def test_errors_out_if_reference_not_in_parameters(self): - ref = Reference.model_construct(ref="#/components/parameters/a_param") - class_info = Class(name="a_param", module_name="module_name") + ref = Reference.model_construct(ref=Ref("#/components/parameters/a_param")) + class_info = Class(name=ClassName(UntrustedString("a_param"), prefix=""), module_name="module_name") existing_param = Parameter.model_construct(name="a_param") - param_by_ref = Reference.model_construct(ref="#/components/parameters/another_param") + param_by_ref = Reference.model_construct(ref=Ref("#/components/parameters/another_param")) params = Parameters( classes_by_name={class_info.name: existing_param}, classes_by_reference={ref.ref: existing_param} ) @@ -95,47 +97,12 @@ def test_errors_out_if_reference_not_in_parameters(self): def test_returns_reference_from_registry(self): existing_param = Parameter.model_construct(name="a_param") - class_info = Class(name="MyParameter", module_name="module_name") + class_info = Class(name=ClassName(UntrustedString("MyParameter"), prefix=""), module_name="module_name") params = Parameters( classes_by_name={class_info.name: existing_param}, - classes_by_reference={"/components/parameters/a_param": existing_param}, + classes_by_reference={ReferencePath(UntrustedString("/components/parameters/a_param")): existing_param}, ) - param_by_ref = Reference.model_construct(ref="#/components/parameters/a_param") + param_by_ref = Reference.model_construct(ref=Ref("#/components/parameters/a_param")) param_or_error = parameter_from_reference(param=param_by_ref, parameters=params) assert param_or_error == existing_param - - -class TestUpdateParametersFromData: - def test_reports_parameters_with_errors(self, mocker, config): - parameters = Parameters() - param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() - ) - parameter_from_data = mocker.patch( - f"{MODULE_NAME}.parameter_from_data", side_effect=[(ParameterError(), parameters)] - ) - ref_path = Reference.model_construct(ref="#/components/parameters/a_param") - new_parameters_or_error = update_parameters_with_data( - ref_path=ref_path.ref, data=param, parameters=parameters, config=config - ) - - parameter_from_data.assert_called_once() - assert new_parameters_or_error == ParameterError( - detail="Unable to parse this part of your OpenAPI document: : None", - header="Unable to parse parameter #/components/parameters/a_param", - ) - - def test_records_references_to_parameters(self, mocker, config): - parameters = Parameters() - param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() - ) - parameter_from_data = mocker.patch(f"{MODULE_NAME}.parameter_from_data", side_effect=[(param, parameters)]) - ref_path = "#/components/parameters/a_param" - new_parameters = update_parameters_with_data( - ref_path=ref_path, data=param, parameters=parameters, config=config - ) - - parameter_from_data.assert_called_once() - assert new_parameters.classes_by_reference[ref_path] == param diff --git a/tests/test_parser/test_properties/test_union.py b/tests/test_parser/test_properties/test_union.py index 902603f2e..b86100b69 100644 --- a/tests/test_parser/test_properties/test_union.py +++ b/tests/test_parser/test_properties/test_union.py @@ -1,4 +1,5 @@ import openapi_python_client.schema as oai +from openapi_python_client import UntrustedString from openapi_python_client.parser.errors import ParseError from openapi_python_client.parser.properties import Schemas, UnionProperty, property_from_data from openapi_python_client.schema import DataType, ParameterLocation @@ -10,7 +11,7 @@ def test_invalid_location(config): ) prop, _ = UnionProperty.build( - data=data, required=True, schemas=Schemas(), parent_name="parent", name="name", config=config + data=data, required=True, schemas=Schemas(), parent_name="parent", name=UntrustedString("name"), config=config ) err = prop.validate_location(ParameterLocation.PATH) @@ -23,7 +24,7 @@ def test_not_required_in_path(config): ) prop, _ = UnionProperty.build( - data=data, required=False, schemas=Schemas(), parent_name="parent", name="name", config=config + data=data, required=False, schemas=Schemas(), parent_name="parent", name=UntrustedString("name"), config=config ) err = prop.validate_location(ParameterLocation.PATH) @@ -75,7 +76,7 @@ def test_union_oneOf_descriptive_type_name( ) p, s = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=config + name=UntrustedString(name), required=required, data=data, schemas=Schemas(), parent_name="parent", config=config ) assert p == expected diff --git a/tests/test_parser/test_responses.py b/tests/test_parser/test_responses.py index 6c0f60e2c..70cbd88d3 100644 --- a/tests/test_parser/test_responses.py +++ b/tests/test_parser/test_responses.py @@ -3,6 +3,7 @@ import pytest import openapi_python_client.schema as oai +from openapi_python_client import UntrustedString from openapi_python_client.parser import responses from openapi_python_client.parser.errors import ParseError, PropertyError from openapi_python_client.parser.properties import Schemas @@ -46,7 +47,7 @@ def test_response_from_data_no_content(any_property_factory): def test_response_from_data_unsupported_content_type(): - data = oai.Response.model_construct(description="", content={"blah": None}) + data = oai.Response.model_construct(description="", content={oai.UntrustedString("blah"): None}) config = MagicMock() config.content_type_overrides = {} response, _schemas = response_from_data( @@ -58,13 +59,13 @@ def test_response_from_data_unsupported_content_type(): config=config, ) - assert response == ParseError(data=data, detail="Unsupported content_type {'blah': None}") + assert response == ParseError(data=data, detail="Unsupported content_type blah") def test_response_from_data_no_content_schema(any_property_factory): data = oai.Response.model_construct( description="", - content={"application/vnd.api+json; version=2.2": oai.MediaType.model_construct()}, + content={oai.UntrustedString("application/vnd.api+json; version=2.2"): oai.MediaType.model_construct()}, ) config = MagicMock() config.content_type_overrides = {} @@ -94,7 +95,7 @@ def test_response_from_data_property_error(mocker): property_from_data = mocker.patch.object(responses, "property_from_data", return_value=(PropertyError(), Schemas())) data = oai.Response.model_construct( description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, + content={oai.UntrustedString("application/json"): oai.MediaType.model_construct(media_type_schema="something")}, ) config = MagicMock() config.content_type_overrides = {} @@ -124,7 +125,7 @@ def test_response_from_data_property(mocker, any_property_factory): property_from_data = mocker.patch.object(responses, "property_from_data", return_value=(prop, Schemas())) data = oai.Response.model_construct( description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, + content={oai.UntrustedString("application/json"): oai.MediaType.model_construct(media_type_schema="something")}, ) config = MagicMock() config.content_type_overrides = {} @@ -155,97 +156,10 @@ def test_response_from_data_property(mocker, any_property_factory): ) -def test_response_from_data_reference(mocker, any_property_factory): - prop = any_property_factory() - mocker.patch.object(responses, "property_from_data", return_value=(prop, Schemas())) - predefined_response_data = oai.Response.model_construct( - description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, - ) - config = MagicMock() - config.content_type_overrides = {} - - response, _schemas = responses.response_from_data( - status_code=HTTPStatusPattern(pattern="400", code_range=(400, 400)), - data=oai.Reference.model_construct(ref="#/components/responses/ErrorResponse"), - schemas=Schemas(), - responses={"ErrorResponse": predefined_response_data}, - parent_name="parent", - config=config, - ) - - assert response == responses.Response( - status_code=HTTPStatusPattern(pattern="400", code_range=(400, 400)), - prop=prop, - source=JSON_SOURCE, - data=predefined_response_data, - ) - - -@pytest.mark.parametrize( - "ref_string,expected_error_string", - [ - ("#/components/responses/Nonexistent", "Could not find"), - ("https://remote-reference", "Remote references"), - ("#/components/something-that-isnt-responses/ErrorResponse", "not allowed in responses"), - ], -) -def test_response_from_data_invalid_reference(ref_string, expected_error_string, mocker, any_property_factory): - prop = any_property_factory() - mocker.patch.object(responses, "property_from_data", return_value=(prop, Schemas())) - predefined_response_data = oai.Response.model_construct( - description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, - ) - config = MagicMock() - config.content_type_overrides = {} - - response, _schemas = responses.response_from_data( - status_code=HTTPStatusPattern(pattern="400", code_range=(400, 400)), - data=oai.Reference.model_construct(ref=ref_string), - schemas=Schemas(), - responses={"ErrorResponse": predefined_response_data}, - parent_name="parent", - config=config, - ) - - assert isinstance(response, ParseError) - assert expected_error_string in response.detail - - -def test_response_from_data_ref_to_response_that_is_a_ref(mocker, any_property_factory): - prop = any_property_factory() - mocker.patch.object(responses, "property_from_data", return_value=(prop, Schemas())) - predefined_response_base_data = oai.Response.model_construct( - description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, - ) - predefined_response_data = oai.Reference.model_construct( - ref="#/components/references/BaseResponse", - ) - config = MagicMock() - config.content_type_overrides = {} - - response, _schemas = responses.response_from_data( - status_code=HTTPStatusPattern(pattern="400", code_range=(400, 400)), - data=oai.Reference.model_construct(ref="#/components/responses/ErrorResponse"), - schemas=Schemas(), - responses={ - "BaseResponse": predefined_response_base_data, - "ErrorResponse": predefined_response_data, - }, - parent_name="parent", - config=config, - ) - - assert isinstance(response, ParseError) - assert response.detail is not None and "Top-level $ref" in response.detail - - def test_response_from_data_content_type_overrides(any_property_factory): data = oai.Response.model_construct( description="", - content={"application/zip": oai.MediaType.model_construct()}, + content={oai.UntrustedString("application/zip"): oai.MediaType.model_construct()}, ) config = MagicMock() config.content_type_overrides = {"application/zip": "application/octet-stream"} @@ -284,8 +198,8 @@ def test_response_from_data_content_type_overrides(any_property_factory): ], ) def test_http_status_pattern_lt(pattern1: str, pattern2: str, result: bool) -> None: - first = HTTPStatusPattern.parse(pattern1) - second = HTTPStatusPattern.parse(pattern2) + first = HTTPStatusPattern.parse(UntrustedString(pattern1)) + second = HTTPStatusPattern.parse(UntrustedString(pattern2)) assert isinstance(first, HTTPStatusPattern) assert isinstance(second, HTTPStatusPattern) assert (first < second) == result diff --git a/tests/test_schema/test_open_api.py b/tests/test_schema/test_open_api.py index bdab19eba..02b4946c1 100644 --- a/tests/test_schema/test_open_api.py +++ b/tests/test_schema/test_open_api.py @@ -2,6 +2,7 @@ from pydantic import ValidationError from openapi_python_client.schema import OpenAPI +from openapi_python_client.schema.untrusted_string import UntrustedString @pytest.mark.parametrize( @@ -40,6 +41,6 @@ def test_parse_with_callback(): } open_api = OpenAPI.model_validate(data) - create_endpoint = open_api.paths["/create"] + create_endpoint = open_api.paths[UntrustedString("/create")] assert "200" in create_endpoint.post.responses assert "200" in create_endpoint.post.callbacks["event"]["callback"].post.responses diff --git a/tests/test_schema/test_untrusted_string.py b/tests/test_schema/test_untrusted_string.py new file mode 100644 index 000000000..54350daf1 --- /dev/null +++ b/tests/test_schema/test_untrusted_string.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel + +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString + + +class Model(BaseModel): + untrusted: UntrustedString + + +class RefModel(BaseModel): + ref: Ref + + +def test_ref_json_serialization(): + """Ref must serialize to its raw value in JSON mode so error messages can include it.""" + m = RefModel.model_validate({"ref": "#/components/schemas/Foo"}) + assert m.model_dump_json() == '{"ref":"#/components/schemas/Foo"}' + + +def test_untrusted_string_deserialization(): + m = Model.model_validate_json('{"untrusted":"hello"}') + assert m.untrusted.get_untrusted_value() == "hello" + + +def test_untrusted_string_json_serialization(): + """UntrustedString must serialize to its raw value in JSON mode so error messages can include it.""" + m = Model.model_validate_json('{"untrusted":"hello"}') + assert m.model_dump_json() == '{"untrusted":"hello"}' + # Python mode keeps the wrapper intact so it can't leak into generated code + assert isinstance(m.model_dump()["untrusted"], UntrustedString) diff --git a/tests/test_strings.py b/tests/test_strings.py new file mode 100644 index 000000000..a90fb6e2a --- /dev/null +++ b/tests/test_strings.py @@ -0,0 +1,328 @@ +import ast +import copy +import textwrap +import warnings + +import pytest + +from openapi_python_client import strings +from openapi_python_client.schema.untrusted_string import UntrustedString + + +class TestPythonIdentifier: + def test_valid_identifier_is_not_changed(self): + assert strings.PythonIdentifier(value="valid_field", prefix="field") == "valid_field" + + def test_numbers_are_prefixed(self): + assert strings.PythonIdentifier(value="1", prefix="field") == "field1" + + def test_invalid_symbols_are_stripped(self): + assert strings.PythonIdentifier(value="$abc", prefix="prefix") == "abc" + + def test_keywords_are_postfixed(self): + assert strings.PythonIdentifier(value="for", prefix="prefix") == "for_" + + def test_empty_is_prefixed(self): + assert strings.PythonIdentifier(value="", prefix="something") == "something" + + def test_skip_snake_case_preserves_case(self): + assert strings.PythonIdentifier(value="fooBar", prefix="field", skip_snake_case=True) == "fooBar" + + def test_skip_snake_case_strips_delimiters(self): + assert strings.PythonIdentifier(value="foo-bar", prefix="field", skip_snake_case=True) == "foobar" + assert strings.PythonIdentifier(value="foo bar", prefix="field", skip_snake_case=True) == "foobar" + assert strings.PythonIdentifier(value="foo.bar", prefix="field", skip_snake_case=True) == "foobar" + + def test_skip_snake_case_keeps_underscores(self): + assert strings.PythonIdentifier(value="foo_bar", prefix="field", skip_snake_case=True) == "foo_bar" + + def test_skip_snake_case_result_is_always_valid_identifier(self): + result = strings.PythonIdentifier(value="foo-bar", prefix="field", skip_snake_case=True) + assert result.isidentifier() + assert strings.PythonIdentifier(value="1-2", prefix="field", skip_snake_case=True).isidentifier() + assert strings.PythonIdentifier(value="---", prefix="field", skip_snake_case=True).isidentifier() + + def test_non_identifier_unicode_is_stripped(self): + # U+00BC VULGAR FRACTION ONE QUARTER matches \w but is not valid in identifiers + assert strings.PythonIdentifier(value="¼", prefix="field") == "field" + assert strings.PythonIdentifier(value="¼", prefix="field").isidentifier() + + +class TestClassName: + def test_valid_is_not_changed(self): + assert strings.ClassName(value=UntrustedString("ValidClass"), prefix="field") == "ValidClass" + + def test_numbers_are_prefixed(self): + assert strings.ClassName(value=UntrustedString("1"), prefix="field") == "Field1" + + def test_invalid_symbols_are_stripped(self): + assert strings.ClassName(value=UntrustedString("$abc"), prefix="prefix") == "Abc" + + def test_keywords_are_postfixed(self): + assert strings.ClassName(value=UntrustedString("none"), prefix="prefix") == "None_" + + def test_empty_is_prefixed(self): + assert strings.ClassName(value=UntrustedString(""), prefix="something") == "Something" + + def test_deepcopy_returns_self(self): + name = strings.ClassName(value=UntrustedString("ValidClass"), prefix="field") + assert copy.deepcopy(name) is name + + +@pytest.mark.parametrize( + "before, after", + [ + ("connectionID", ["connection", "ID"]), + ("connection_id", ["connection", "id"]), + ("connection-id", ["connection", "id"]), + ("Response200", ["Response", "200"]), + ("Response200Okay", ["Response", "200", "Okay"]), + ("S3Config", ["S3", "Config"]), + ("s3config", ["s3config"]), + ("fully.qualified.Name", ["fully", "qualified", "Name"]), + ], +) +def test_split_words(before, after): + assert strings.split_words(before) == after + + +def test_snake_case_uppercase_str(): + assert strings.snake_case("HTTP") == "http" + assert strings.snake_case("HTTP RESPONSE") == "http_response" + + +def test_snake_case_from_pascal_with_acronyms(): + assert strings.snake_case("HTTPResponse") == "http_response" + assert strings.snake_case("APIClientHTTPResponse") == "api_client_http_response" + assert strings.snake_case("OAuthClientHTTPResponse") == "o_auth_client_http_response" + assert strings.snake_case("S3Config") == "s3_config" + + +def test_snake_case_from_pascal_with_numbers(): + assert strings.snake_case("Response200") == "response_200" + assert strings.snake_case("Response200WithContent") == "response_200_with_content" + + +def test_snake_case_from_pascal(): + assert strings.snake_case("HttpResponsePascalCase") == "http_response_pascal_case" + + +def test_snake_case_from_camel(): + assert strings.snake_case("httpResponseLowerCamel") == "http_response_lower_camel" + assert strings.snake_case("connectionID") == "connection_id" + + +def test_snake_case_untrusted_string(): + assert strings.snake_case(UntrustedString("httpResponse")) == "http_response" + + +def test_kebab_case(): + assert strings.kebab_case("keep_alive") == "keep-alive" + + +def test_sanitize(): + assert strings.sanitize("some.thing*~with lots_- of weird things}=") == "some.thingwith lots_- of weird things" + + +def test_sanitize_strips_non_identifier_unicode(): + # ¼ matches \w but cannot appear in a Python identifier; digits must be kept for splitting + assert strings.sanitize("¼") == "" + assert strings.sanitize("S3Config") == "S3Config" + + +def test_split_words_on_backslash(): + # Backslash is a delimiter; it must actually split, not leak into the word + assert strings.split_words("one\\two") == ["one", "two"] + assert strings.snake_case("one\\two") == "one_two" + + +@pytest.mark.parametrize( + "value, expected", + [ + ('an "evil" string', r"an \"evil\" string"), + (r"an \"evil string", r"an \\\"evil string"), + ], +) +def test_no_string_escapes(value: str, expected: str): + assert strings.remove_string_escapes(value) == expected + + +@pytest.mark.parametrize( + "reserved_word, expected", + [ + ("self", "self_"), + ("int", "int_"), + ("dict", "dict_"), + ("not_reserved", "not_reserved"), + ("type", "type_"), + ("id", "id"), + ("None", "None_"), + ], +) +def test__fix_reserved_words(reserved_word: str, expected: str): + assert strings.fix_reserved_words(reserved_word) == expected + + +@pytest.mark.parametrize( + "before, after", + [ + ("PascalCase", "PascalCase"), + ("snake_case", "SnakeCase"), + ("TLAClass", "TLAClass"), + ("Title Case", "TitleCase"), + ("s3_config", "S3Config"), + ("__LeadingUnderscore", "LeadingUnderscore"), + ], +) +def test_pascalcase(before, after): + assert strings.pascal_case(before) == after + + +@pytest.mark.parametrize( + "content_type, expected", + [ + pytest.param("application/json", "application/json"), + pytest.param("application/vnd.api+json", "application/vnd.api+json"), + pytest.param("application/json;charset=utf-8", "application/json"), + pytest.param("application/vnd.api+json;charset=utf-8", "application/vnd.api+json"), + ], +) +def test_get_content_type(content_type: str, expected: str, config) -> None: + assert strings.get_content_type(content_type, config) == expected + + +@pytest.mark.parametrize( + "untrusted, expected", + [ + ("innocuous", "innocuous"), + ('can contain "quotes"', 'can contain "quotes"'), + ("backslash\\ should be escaped", "backslash\\\\ should be escaped"), + ('triple quotes """ are an issue', 'triple quotes \\"\\"\\" are an issue'), + # Control characters which are illegal in Python source must be stripped + ("null\x00byte", "nullbyte"), + ("bell\x07rings", "bellrings"), + ("newlines\nand\ttabs\r\nstay", "newlines\nand\ttabs\r\nstay"), + ], +) +def test_safe_for_docstring(untrusted: str, expected: str) -> None: + assert strings.safe_for_docstring(UntrustedString(untrusted)) == expected + + +def test_safe_for_docstring_non_string() -> None: + """Non-string values (e.g. dict examples) are stringified instead of crashing""" + assert strings.safe_for_docstring({"nested": 'has "quotes"'}) == "{'nested': 'has \"quotes\"'}" + + +@pytest.mark.parametrize( + "untrusted, expected", + [ + ("innocuous", "innocuous"), + ('quotes "break out" of literals', 'quotes \\"break out\\" of literals'), + ("backslash\\ escapes", "backslash\\\\ escapes"), + ("newlines\nand\ttabs", "newlinesand\\ttabs"), + ("backspace\bform-feed\f", "backspace\\bform-feed\\f"), + ("single 'quotes' are fine", "single 'quotes' are fine"), + ("astral character \U00010000", "astral character \U00010000"), + ("\x7f\N{NEXT LINE}\N{LINE SEPARATOR}\N{PARAGRAPH SEPARATOR}", ""), + ("bidirectional\u202etext", "bidirectionaltext"), + ], +) +def test_in_double_quote_literal(untrusted: str, expected: str) -> None: + assert strings.in_double_quote_literal(UntrustedString(untrusted)) == expected + + +@pytest.mark.parametrize( + "untrusted, expected", + [ + ("innocuous", "innocuous"), + ('quotes "break out"', 'quotes \\"break out\\"'), + ("{braces} inject code", "{{braces}} inject code"), + ("{x!r:>10}", "{{x!r:>10}}"), + ("backslash\\", "backslash\\\\"), + ], +) +def test_in_f_string_literal(untrusted: str, expected: str) -> None: + assert strings.in_f_string_literal(UntrustedString(untrusted)) == expected + + +class TestPythonCode: + def test_get_code_returns_raw_source(self) -> None: + assert strings.PythonCode("'some' + code").as_unembedded_code() == "'some' + code" + + def test_repr_does_not_contain_code(self) -> None: + """The repr is what a template that forgets to unwrap renders: it must never contain the code""" + code = strings.PythonCode('""" + print("uh oh")') + assert "print" not in repr(code) + assert "PythonCode object at 0x" in repr(code) + + def test_no_str(self) -> None: + """PythonCode must not be implicitly convertible to str, so str() falls back to the safe repr""" + assert "__str__" not in vars(strings.PythonCode) + code = strings.PythonCode("1 + 1") + assert str(code) == repr(code) + assert "1 + 1" not in str(code) + + +def test_in_double_quote_literal_accepts_python_code() -> None: + assert strings.in_double_quote_literal(strings.PythonCode('"quoted"')) == '\\"quoted\\"' + + +def test_safe_for_docstring_accepts_python_code() -> None: + assert strings.safe_for_docstring(strings.PythonCode('a"""b')) == 'a\\"\\"\\"b' + + +def _wordwrap_like_templates(text: str, width: int) -> str: + """Replicates the `wordwrap` filter as used in templates (per-paragraph textwrap)""" + return "\n".join( + "\n".join( + textwrap.wrap( + line, + width=width, + expand_tabs=False, + replace_whitespace=False, + break_long_words=True, + break_on_hyphens=True, + ) + ) + for line in text.splitlines() + ) + + +# Widths passed to the wordwrap filter in templates +@pytest.mark.parametrize("width", [90, 100, 101, 112, 116]) +def test_safe_for_docstring_output_survives_wordwrap(width: int) -> None: + """wordwrap hard-splits over-long words; escaped content must stay valid Python wherever it lands""" + nasty = ( + 'has """ triple \\ quotes and \U001060ef astral chars and \x00nulls ' * 5 + # One long unbroken word: \U and \N are hard SyntaxErrors if a split leaves them dangling + + r"C:\Users\New\Documents\foo" * 8 + + " " + + '"' * 200 + ) + escaped = strings.safe_for_docstring(nasty) + wrapped = _wordwrap_like_templates(escaped, width) + with warnings.catch_warnings(): + warnings.simplefilter("error") # invalid escape sequences would warn (SyntaxError in future Python) + parsed = ast.parse(f'"""\n{wrapped}\n"""') + docstring = parsed.body[0].value.value + assert isinstance(docstring, str) # nothing broke out of the docstring + + +def test_safe_for_docstring_chunks_long_words_at_escape_safe_points() -> None: + """Inserted line breaks must not interrupt an escape sequence or change the content""" + word = r"C:\Users\New\Documents\foo" * 8 + escaped = strings.safe_for_docstring(word) + lines = escaped.split("\n") + assert len(lines) > 1 + assert all(len(line) <= strings._MAX_ESCAPED_WORD_LENGTH for line in lines) + for line in lines: + trailing_backslashes = len(line) - len(line.rstrip("\\")) + assert trailing_backslashes % 2 == 0 + # Only newlines were inserted; the escaped content is otherwise untouched + assert escaped.replace("\n", "") == word.replace("\\", "\\\\") + + +def test_safe_for_docstring_does_not_chunk_words_without_backslashes() -> None: + """Words with no escape sequences (e.g. URLs) can be hard-split anywhere, so leave them alone""" + word = "https://example.com/" + "a" * 500 + assert strings.safe_for_docstring(word) == word diff --git a/tests/test_templates/conftest.py b/tests/test_templates/conftest.py index 09fd63f6c..e41b84308 100644 --- a/tests/test_templates/conftest.py +++ b/tests/test_templates/conftest.py @@ -4,9 +4,8 @@ @pytest.fixture(scope="session") def env() -> Environment: - from openapi_python_client import utils + from openapi_python_client import TEMPLATE_FILTERS - TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case} env = Environment(loader=PackageLoader("openapi_python_client"), trim_blocks=True, lstrip_blocks=True) env.filters.update(TEMPLATE_FILTERS) return env diff --git a/tests/test_templates/test_endpoint_macros.py b/tests/test_templates/test_endpoint_macros.py new file mode 100644 index 000000000..c601e0e30 --- /dev/null +++ b/tests/test_templates/test_endpoint_macros.py @@ -0,0 +1,36 @@ +from types import SimpleNamespace + +from openapi_python_client.strings import PythonCode + + +def _render_docstring(env, return_string, is_detailed: bool) -> str: + template = env.get_template("endpoint_macros.py.jinja") + endpoint = SimpleNamespace(summary=None, description=None, list_all_parameters=lambda: []) + return template.module.docstring(endpoint, return_string, is_detailed) + + +def test_endpoint_docstring_escapes_return_string(env) -> None: + """The return type is rendered inside endpoint docstrings; it must not be able to break out. + + Type strings can embed literal values (e.g. const `Literal[...]`), so the docstring macro must pass the + return string through `safe_for_docstring` rather than rendering the raw code. Note that a raw breakout + would still be *valid* Python (that's the point of an escape), so this asserts on the docstring content, + not just parseability. + """ + nasty = 'Literal[\'""" + print("uh oh") + """\']' + + for is_detailed in (True, False): + result = _render_docstring(env, nasty, is_detailed) + # Only the two docstring delimiters; the value's triple-quotes must be escaped + assert result.count('"""') == 2 + assert '\\"\\"\\"' in result + + +def test_endpoint_docstring_unwraps_python_code_return_string(env) -> None: + """A PythonCode return type must render its (escaped) code in the docstring, not the safe pointer repr""" + code = PythonCode("MyModel | None") + + result = _render_docstring(env, code, is_detailed=False) + + assert "MyModel | None" in result + assert "object at 0x" not in result diff --git a/tests/test_templates/test_model_template.py b/tests/test_templates/test_model_template.py index b80491197..1443f6c98 100644 --- a/tests/test_templates/test_model_template.py +++ b/tests/test_templates/test_model_template.py @@ -2,6 +2,8 @@ from types import SimpleNamespace +from openapi_python_client import UntrustedString + def _section(content: str, start: str, end: str | None = None) -> str: section = content.split(start, 1)[1] @@ -19,9 +21,9 @@ def test_model_template_renders_lazy_imports_in_stable_order(env) -> None: lazy_imports={"from ..models.z import Z", "from ..models.a import A"}, additional_properties=False, class_info=SimpleNamespace(name="MyModel", module_name="my_model"), - title="", - description="", - example="", + title=UntrustedString(""), + description=UntrustedString(""), + example=UntrustedString(""), required_properties=[], optional_properties=[], ) diff --git a/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py b/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py index fe7cd7f85..224b7b855 100644 --- a/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py +++ b/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py @@ -8,7 +8,7 @@ import pytest from openapi_python_client.parser.properties import UuidProperty -from openapi_python_client.utils import PythonIdentifier +from openapi_python_client.strings import PythonIdentifier def uuid_property(required: bool = True, default: Any = None) -> UuidProperty: diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index fafa61805..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest - -from openapi_python_client import utils - - -class TestPythonIdentifier: - def test_valid_identifier_is_not_changed(self): - assert utils.PythonIdentifier(value="valid_field", prefix="field") == "valid_field" - - def test_numbers_are_prefixed(self): - assert utils.PythonIdentifier(value="1", prefix="field") == "field1" - - def test_invalid_symbols_are_stripped(self): - assert utils.PythonIdentifier(value="$abc", prefix="prefix") == "abc" - - def test_keywords_are_postfixed(self): - assert utils.PythonIdentifier(value="for", prefix="prefix") == "for_" - - def test_empty_is_prefixed(self): - assert utils.PythonIdentifier(value="", prefix="something") == "something" - - -class TestClassName: - def test_valid_is_not_changed(self): - assert utils.ClassName(value="ValidClass", prefix="field") == "ValidClass" - - def test_numbers_are_prefixed(self): - assert utils.ClassName(value="1", prefix="field") == "Field1" - - def test_invalid_symbols_are_stripped(self): - assert utils.ClassName(value="$abc", prefix="prefix") == "Abc" - - def test_keywords_are_postfixed(self): - assert utils.ClassName(value="none", prefix="prefix") == "None_" - - def test_empty_is_prefixed(self): - assert utils.ClassName(value="", prefix="something") == "Something" - - -@pytest.mark.parametrize( - "before, after", - [ - ("connectionID", ["connection", "ID"]), - ("connection_id", ["connection", "id"]), - ("connection-id", ["connection", "id"]), - ("Response200", ["Response", "200"]), - ("Response200Okay", ["Response", "200", "Okay"]), - ("S3Config", ["S3", "Config"]), - ("s3config", ["s3config"]), - ("fully.qualified.Name", ["fully", "qualified", "Name"]), - ], -) -def test_split_words(before, after): - assert utils.split_words(before) == after - - -def test_snake_case_uppercase_str(): - assert utils.snake_case("HTTP") == "http" - assert utils.snake_case("HTTP RESPONSE") == "http_response" - - -def test_snake_case_from_pascal_with_acronyms(): - assert utils.snake_case("HTTPResponse") == "http_response" - assert utils.snake_case("APIClientHTTPResponse") == "api_client_http_response" - assert utils.snake_case("OAuthClientHTTPResponse") == "o_auth_client_http_response" - assert utils.snake_case("S3Config") == "s3_config" - - -def test_snake_case_from_pascal_with_numbers(): - assert utils.snake_case("Response200") == "response_200" - assert utils.snake_case("Response200WithContent") == "response_200_with_content" - - -def test_snake_case_from_pascal(): - assert utils.snake_case("HttpResponsePascalCase") == "http_response_pascal_case" - - -def test_snake_case_from_camel(): - assert utils.snake_case("httpResponseLowerCamel") == "http_response_lower_camel" - assert utils.snake_case("connectionID") == "connection_id" - - -def test_kebab_case(): - assert utils.kebab_case("keep_alive") == "keep-alive" - - -def test_sanitize(): - assert utils.sanitize("some.thing*~with lots_- of weird things}=") == "some.thingwith lots_- of weird things" - - -def test_no_string_escapes(): - assert utils.remove_string_escapes('an "evil" string') == 'an \\"evil\\" string' - - -@pytest.mark.parametrize( - "reserved_word, expected", - [ - ("self", "self_"), - ("int", "int_"), - ("dict", "dict_"), - ("not_reserved", "not_reserved"), - ("type", "type_"), - ("id", "id"), - ("None", "None_"), - ], -) -def test__fix_reserved_words(reserved_word: str, expected: str): - assert utils.fix_reserved_words(reserved_word) == expected - - -@pytest.mark.parametrize( - "before, after", - [ - ("PascalCase", "PascalCase"), - ("snake_case", "SnakeCase"), - ("TLAClass", "TLAClass"), - ("Title Case", "TitleCase"), - ("s3_config", "S3Config"), - ("__LeadingUnderscore", "LeadingUnderscore"), - ], -) -def test_pascalcase(before, after): - assert utils.pascal_case(before) == after - - -@pytest.mark.parametrize( - "content_type, expected", - [ - pytest.param("application/json", "application/json"), - pytest.param("application/vnd.api+json", "application/vnd.api+json"), - pytest.param("application/json;charset=utf-8", "application/json"), - pytest.param("application/vnd.api+json;charset=utf-8", "application/vnd.api+json"), - ], -) -def test_get_content_type(content_type: str, expected: str, config) -> None: - assert utils.get_content_type(content_type, config) == expected