├── tests ├── __init__.py ├── test_templates │ ├── __init__.py │ ├── test_property_templates │ │ ├── test_uuid_property │ │ │ └── __init__.py │ │ ├── test_datetime_property │ │ │ ├── required_not_null.py │ │ │ ├── datetime_property_template.py.jinja │ │ │ └── test_datetime_property.py │ │ └── test_date_property │ │ │ ├── required_not_null.py │ │ │ ├── date_property_template.py.jinja │ │ │ └── test_date_property.py │ └── conftest.py ├── test_parser │ ├── test_properties │ │ ├── test_file.py │ │ ├── test_none.py │ │ └── test_protocol.py │ └── test_bodies.py ├── test_schema │ ├── test_data_type.py │ └── test_open_api.py └── test_cli.py ├── openapi_python_client ├── py.typed ├── __main__.py ├── templates │ ├── api_init.py.jinja │ ├── endpoint_init.py.jinja │ ├── pyproject_ruff.toml.jinja │ ├── property_templates │ │ ├── any_property.py.jinja │ │ ├── int_property.py.jinja │ │ ├── float_property.py.jinja │ │ ├── boolean_property.py.jinja │ │ ├── helpers.jinja │ │ ├── const_property.py.jinja │ │ ├── property_macros.py.jinja │ │ ├── file_property.py.jinja │ │ ├── uuid_property.py.jinja │ │ ├── date_property.py.jinja │ │ ├── datetime_property.py.jinja │ │ ├── enum_property.py.jinja │ │ ├── literal_enum_property.py.jinja │ │ └── model_property.py.jinja │ ├── package_init.py.jinja │ ├── int_enum.py.jinja │ ├── str_enum.py.jinja │ ├── models_init.py.jinja │ ├── pyproject.toml.jinja │ ├── .gitignore.jinja │ ├── helpers.jinja │ ├── pyproject_pdm.toml.jinja │ ├── pyproject_uv.toml.jinja │ ├── pyproject_poetry.toml.jinja │ ├── errors.py.jinja │ ├── setup.py.jinja │ ├── literal_enum.py.jinja │ └── types.py.jinja ├── parser │ ├── __init__.py │ ├── properties │ │ ├── property.py │ │ ├── any.py │ │ ├── none.py │ │ └── string.py │ └── errors.py └── schema │ ├── data_type.py │ ├── openapi_schema_pydantic │ ├── paths.py │ ├── server_variable.py │ ├── license.py │ ├── external_documentation.py │ ├── callback.py │ ├── oauth_flows.py │ ├── contact.py │ ├── tag.py │ ├── responses.py │ ├── security_requirement.py │ ├── xml.py │ ├── example.py │ ├── LICENSE │ ├── oauth_flow.py │ ├── discriminator.py │ ├── header.py │ ├── encoding.py │ ├── server.py │ ├── info.py │ ├── reference.py │ ├── open_api.py │ ├── link.py │ ├── security_scheme.py │ └── README.md │ ├── __init__.py │ └── parameter_location.py ├── end_to_end_tests ├── literal_enums.config.yml ├── test_custom_templates │ ├── README.md.jinja │ ├── api_init.py.jinja │ ├── endpoint_init.py.jinja │ └── models_init.py.jinja ├── custom-templates-golden-record │ ├── README.md │ └── my_test_api_client │ │ ├── api │ │ ├── true_ │ │ │ └── __init__.py │ │ ├── tag1 │ │ │ └── __init__.py │ │ ├── tag2 │ │ │ └── __init__.py │ │ ├── config │ │ │ └── __init__.py │ │ ├── defaults │ │ │ └── __init__.py │ │ ├── parameter_references │ │ │ └── __init__.py │ │ ├── location │ │ │ └── __init__.py │ │ ├── enums │ │ │ └── __init__.py │ │ ├── naming │ │ │ └── __init__.py │ │ ├── bodies │ │ │ └── __init__.py │ │ ├── parameters │ │ │ └── __init__.py │ │ ├── default │ │ │ └── __init__.py │ │ └── responses │ │ │ └── __init__.py │ │ └── models │ │ └── __init__.py ├── golden-record │ ├── my_test_api_client │ │ ├── py.typed │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── bodies │ │ │ │ └── __init__.py │ │ │ ├── config │ │ │ │ └── __init__.py │ │ │ ├── default │ │ │ │ └── __init__.py │ │ │ ├── enums │ │ │ │ └── __init__.py │ │ │ ├── naming │ │ │ │ └── __init__.py │ │ │ ├── tag1 │ │ │ │ └── __init__.py │ │ │ ├── tag2 │ │ │ │ └── __init__.py │ │ │ ├── tests │ │ │ │ └── __init__.py │ │ │ ├── true_ │ │ │ │ └── __init__.py │ │ │ ├── defaults │ │ │ │ └── __init__.py │ │ │ ├── location │ │ │ │ └── __init__.py │ │ │ ├── parameters │ │ │ │ └── __init__.py │ │ │ ├── responses │ │ │ │ └── __init__.py │ │ │ └── parameter_references │ │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── models │ │ │ ├── another_all_of_sub_model_type.py │ │ │ ├── another_all_of_sub_model_type_enum.py │ │ │ ├── different_enum.py │ │ │ ├── all_of_sub_model_type_enum.py │ │ │ ├── an_enum.py │ │ │ ├── an_int_enum.py │ │ │ ├── an_enum_with_null.py │ │ │ ├── model_with_merged_properties_string_to_enum.py │ │ │ ├── all_of_has_properties_but_no_type_type_enum.py │ │ │ ├── status_code_patterns_response_2xx_status.py │ │ │ ├── get_location_header_types_int_enum_header.py │ │ │ ├── an_all_of_enum.py │ │ │ ├── get_location_header_types_string_enum_header.py │ │ │ ├── model_with_no_properties.py │ │ │ ├── test_inline_objects_body.py │ │ │ ├── test_inline_objects_response_200.py │ │ │ ├── validation_error.py │ │ │ ├── none.py │ │ │ ├── import_.py │ │ │ ├── model_name.py │ │ │ ├── free_form_model.py │ │ │ ├── optional_body_body.py │ │ │ ├── model_reference_with_periods.py │ │ │ ├── model_with_backslash_in_description.py │ │ │ ├── model_with_any_json_properties_additional_property_type_0.py │ │ │ ├── post_responses_unions_simple_before_complex_response_200a_type_1.py │ │ │ ├── json_like_body.py │ │ │ ├── http_validation_error.py │ │ │ ├── post_bodies_multiple_data_body.py │ │ │ ├── post_bodies_multiple_json_body.py │ │ │ ├── status_code_patterns_response_4xx.py │ │ │ ├── model_with_additional_properties_refed.py │ │ │ ├── a_discriminated_union_type_1.py │ │ │ ├── a_discriminated_union_type_2.py │ │ │ ├── body_upload_file_tests_upload_post_some_optional_object.py │ │ │ ├── model_with_union_property_inlined_apples.py │ │ │ ├── model_with_union_property_inlined_bananas.py │ │ │ ├── body_upload_file_tests_upload_post_additional_property.py │ │ │ ├── body_upload_file_tests_upload_post_some_nullable_object.py │ │ │ ├── body_upload_file_tests_upload_post_some_object.py │ │ │ ├── model_with_primitive_additional_properties_a_date_holder.py │ │ │ ├── model_with_recursive_ref_in_additional_properties.py │ │ │ ├── model_with_union_property.py │ │ │ ├── model_with_additional_properties_inlined_additional_property.py │ │ │ └── mixed_case_response_200.py │ │ ├── errors.py │ │ └── types.py │ ├── .gitignore │ └── pyproject.toml ├── docstrings_on_attributes.config.yml ├── custom_post_hooks.config.yml ├── literal-enums-golden-record │ ├── my_enum_api_client │ │ ├── py.typed │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── enums │ │ │ │ └── __init__.py │ │ │ └── tests │ │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── models │ │ │ ├── an_enum.py │ │ │ ├── an_int_enum.py │ │ │ ├── different_enum.py │ │ │ ├── an_enum_with_null.py │ │ │ ├── an_all_of_enum.py │ │ │ ├── get_user_list_int_enum_header.py │ │ │ ├── get_user_list_string_enum_header.py │ │ │ └── __init__.py │ │ ├── errors.py │ │ └── types.py │ ├── .gitignore │ └── pyproject.toml ├── test-3-1-golden-record │ ├── test_3_1_features_client │ │ ├── py.typed │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── const │ │ │ │ └── __init__.py │ │ │ └── prefix_items │ │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── models │ │ │ └── __init__.py │ │ ├── errors.py │ │ └── types.py │ ├── .gitignore │ └── pyproject.toml ├── docstrings-on-attributes-golden-record │ ├── my_test_api_client │ │ ├── py.typed │ │ ├── api │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── models │ │ │ └── __init__.py │ │ ├── errors.py │ │ └── types.py │ ├── .gitignore │ └── pyproject.toml ├── __init__.py ├── documents_with_errors │ ├── bad-status-code.yaml │ ├── missing-body-ref.yaml │ ├── optional-path-param.yaml │ ├── circular-body-ref.yaml │ └── invalid-uuid-defaults.yaml ├── config.yml ├── metadata_snapshots │ ├── pdm.pyproject.toml │ ├── uv.pyproject.toml │ ├── poetry.pyproject.toml │ └── setup.py ├── functional_tests │ └── generator_failure_cases │ │ ├── test_invalid_arrays.py │ │ ├── test_invalid_references.py │ │ └── test_invalid_unions.py └── docstrings_on_attributes.yml ├── integration-tests ├── integration_tests │ ├── py.typed │ ├── api │ │ ├── __init__.py │ │ ├── body │ │ │ └── __init__.py │ │ └── parameters │ │ │ └── __init__.py │ ├── __init__.py │ ├── errors.py │ ├── models │ │ ├── __init__.py │ │ ├── an_object.py │ │ └── problem.py │ └── types.py ├── config.yaml ├── tests │ ├── conftest.py │ └── test_api │ │ └── test_parameters │ │ └── test_post_parameters_header.py ├── .gitignore └── pyproject.toml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── workflows │ └── release.yml └── renovate.json ├── knope.toml ├── SECURITY.md ├── .gitignore └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openapi_python_client/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 2 | -------------------------------------------------------------------------------- /end_to_end_tests/literal_enums.config.yml: -------------------------------------------------------------------------------- 1 | literal_enums: true 2 | -------------------------------------------------------------------------------- /end_to_end_tests/test_custom_templates/README.md.jinja: -------------------------------------------------------------------------------- 1 | {{ project_name }} -------------------------------------------------------------------------------- /integration-tests/integration_tests/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 -------------------------------------------------------------------------------- /openapi_python_client/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import app 2 | 3 | app() 4 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/README.md: -------------------------------------------------------------------------------- 1 | my-test-api-client -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 -------------------------------------------------------------------------------- /end_to_end_tests/docstrings_on_attributes.config.yml: -------------------------------------------------------------------------------- 1 | docstrings_on_attributes: true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dbanty] 4 | -------------------------------------------------------------------------------- /end_to_end_tests/custom_post_hooks.config.yml: -------------------------------------------------------------------------------- 1 | post_hooks: 2 | - echo "this should fail" && exit 1 -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 -------------------------------------------------------------------------------- /integration-tests/integration_tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API""" 2 | -------------------------------------------------------------------------------- /openapi_python_client/templates/api_init.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains methods for accessing the API """ 2 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API""" 2 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/api/body/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /openapi_python_client/templates/endpoint_init.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains endpoint functions for accessing the API """ 2 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/api/parameters/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_uuid_property/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for UUID property templates.""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/bodies/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/config/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/enums/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/tag1/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/tag2/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/defaults/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/location/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/parameters/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/responses/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /integration-tests/config.yaml: -------------------------------------------------------------------------------- 1 | project_name_override: integration-tests 2 | post_hooks: 3 | - ruff check . --fix 4 | - ruff format . -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains endpoint functions for accessing the API""" 2 | -------------------------------------------------------------------------------- /openapi_python_client/templates/pyproject_ruff.toml.jinja: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | 4 | [tool.ruff.lint] 5 | select = ["F", "I", "UP"] 6 | -------------------------------------------------------------------------------- /integration-tests/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from integration_tests.client import Client 4 | 5 | 6 | @pytest.fixture 7 | def client() -> Client: 8 | return Client("http://localhost:3000") 9 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/any_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro multipart(property, source, name) %} 2 | files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) 3 | {% endmacro %} -------------------------------------------------------------------------------- /openapi_python_client/parser/__init__.py: -------------------------------------------------------------------------------- 1 | """Classes representing the data in the OpenAPI schema""" 2 | 3 | __all__ = ["GeneratorData", "import_string_from_class"] 4 | 5 | from .openapi import GeneratorData, import_string_from_class 6 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/__init__.py: -------------------------------------------------------------------------------- 1 | """A client library for accessing OpenAPI Test Server""" 2 | 3 | from .client import AuthenticatedClient, Client 4 | 5 | __all__ = ( 6 | "AuthenticatedClient", 7 | "Client", 8 | ) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/__init__.py: -------------------------------------------------------------------------------- 1 | """A client library for accessing My Test API""" 2 | 3 | from .client import AuthenticatedClient, Client 4 | 5 | __all__ = ( 6 | "AuthenticatedClient", 7 | "Client", 8 | ) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/__init__.py: -------------------------------------------------------------------------------- 1 | """A client library for accessing My Enum API""" 2 | 3 | from .client import AuthenticatedClient, Client 4 | 5 | __all__ = ( 6 | "AuthenticatedClient", 7 | "Client", 8 | ) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/__init__.py: -------------------------------------------------------------------------------- 1 | """A client library for accessing Test 3.1 Features""" 2 | 3 | from .client import AuthenticatedClient, Client 4 | 5 | __all__ = ( 6 | "AuthenticatedClient", 7 | "Client", 8 | ) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py: -------------------------------------------------------------------------------- 1 | """A client library for accessing My Test API""" 2 | 3 | from .client import AuthenticatedClient, Client 4 | 5 | __all__ = ( 6 | "AuthenticatedClient", 7 | "Client", 8 | ) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AnotherAllOfSubModelType(str, Enum): 5 | SUBMODEL = "submodel" 6 | 7 | def __str__(self) -> str: 8 | return str(self.value) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class AnotherAllOfSubModelTypeEnum(IntEnum): 5 | VALUE_0 = 0 6 | 7 | def __str__(self) -> str: 8 | return str(self.value) 9 | -------------------------------------------------------------------------------- /end_to_end_tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ Generate a complete client and verify that it is correct """ 2 | import pytest 3 | 4 | pytest.register_assert_rewrite("end_to_end_tests.end_to_end_test_helpers") 5 | pytest.register_assert_rewrite("end_to_end_tests.functional_tests.helpers") 6 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/different_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class DifferentEnum(str, Enum): 5 | DIFFERENT = "DIFFERENT" 6 | OTHER = "OTHER" 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class AllOfSubModelTypeEnum(IntEnum): 5 | VALUE_0 = 0 6 | VALUE_1 = 1 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/an_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AnEnum(str, Enum): 5 | FIRST_VALUE = "FIRST_VALUE" 6 | SECOND_VALUE = "SECOND_VALUE" 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/an_int_enum.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class AnIntEnum(IntEnum): 5 | VALUE_NEGATIVE_1 = -1 6 | VALUE_1 = 1 7 | VALUE_2 = 2 8 | 9 | def __str__(self) -> str: 10 | return str(self.value) 11 | -------------------------------------------------------------------------------- /openapi_python_client/templates/package_init.py.jinja: -------------------------------------------------------------------------------- 1 | {% from "helpers.jinja" import safe_docstring %} 2 | 3 | {{ safe_docstring(package_description) }} 4 | from .client import AuthenticatedClient, Client 5 | 6 | __all__ = ( 7 | "AuthenticatedClient", 8 | "Client", 9 | ) 10 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_datetime_property/required_not_null.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from dateutil.parser import isoparse 4 | some_source = date(2020, 10, 12) 5 | some_destination = some_source.isoformat() 6 | a_prop = isoparse(some_destination) 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_date_property/required_not_null.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from dateutil.parser import isoparse 4 | some_source = date(2020, 10, 12) 5 | some_destination = some_source.isoformat() 6 | a_prop = isoparse(some_destination).date() 7 | 8 | 9 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AnEnumWithNull(str, Enum): 5 | FIRST_VALUE = "FIRST_VALUE" 6 | SECOND_VALUE = "SECOND_VALUE" 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/int_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro transform_header(source) %} 2 | str({{ source }}) 3 | {% endmacro %} 4 | 5 | {% macro multipart(property, source, name) %} 6 | files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) 7 | {% endmacro %} 8 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/float_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro transform_header(source) %} 2 | str({{ source }}) 3 | {% endmacro %} 4 | 5 | {% macro multipart(property, source, name) %} 6 | files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) 7 | {% endmacro %} 8 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties_string_to_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ModelWithMergedPropertiesStringToEnum(str, Enum): 5 | A = "a" 6 | B = "b" 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class AllOfHasPropertiesButNoTypeTypeEnum(IntEnum): 5 | VALUE_0 = 0 6 | VALUE_1 = 1 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /knope.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | versioned_files = ["pyproject.toml"] 3 | changelog = "CHANGELOG.md" 4 | extra_changelog_sections = [ 5 | { name = "Notes", types = ["note"] } 6 | ] 7 | 8 | [github] 9 | owner = "openapi-generators" 10 | repo = "openapi-python-client" 11 | 12 | [bot.releases] 13 | enabled = true 14 | -------------------------------------------------------------------------------- /openapi_python_client/templates/int_enum.py.jinja: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | class {{ enum.class_info.name }}(IntEnum): 4 | {% for key, value in enum.values.items() %} 5 | {{ key }} = {{ value }} 6 | {% endfor %} 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /openapi_python_client/templates/str_enum.py.jinja: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class {{ enum.class_info.name }}(str, Enum): 4 | {% for key, value in enum.values|dictsort(true) %} 5 | {{ key }} = "{{ value }}" 6 | {% endfor %} 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/boolean_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro transform_header(source) %} 2 | "true" if {{ source }} else "false" 3 | {% endmacro %} 4 | 5 | {% macro multipart(property, source, name) %} 6 | files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) 7 | {% endmacro %} 8 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_2xx_status.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class StatusCodePatternsResponse2XXStatus(str, Enum): 5 | FAILURE = "failure" 6 | SUCCESS = "success" 7 | 8 | def __str__(self) -> str: 9 | return str(self.value) 10 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import false_ 6 | 7 | 8 | class True_Endpoints: 9 | @classmethod 10 | def false_(cls) -> types.ModuleType: 11 | return false_ 12 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_int_enum_header.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class GetLocationHeaderTypesIntEnumHeader(IntEnum): 5 | VALUE_1 = 1 6 | VALUE_2 = 2 7 | VALUE_3 = 3 8 | 9 | def __str__(self) -> str: 10 | return str(self.value) 11 | -------------------------------------------------------------------------------- /integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | 14 | # mypy 15 | .mypy_cache/ 16 | .dmypy.json 17 | dmypy.json 18 | 19 | # JetBrains 20 | .idea/ 21 | 22 | /coverage.xml 23 | /.coverage 24 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | 14 | # mypy 15 | .mypy_cache/ 16 | .dmypy.json 17 | dmypy.json 18 | 19 | # JetBrains 20 | .idea/ 21 | 22 | /coverage.xml 23 | /.coverage 24 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/an_all_of_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AnAllOfEnum(str, Enum): 5 | A_DEFAULT = "a_default" 6 | BAR = "bar" 7 | FOO = "foo" 8 | OVERRIDDEN_DEFAULT = "overridden_default" 9 | 10 | def __str__(self) -> str: 11 | return str(self.value) 12 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_string_enum_header.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class GetLocationHeaderTypesStringEnumHeader(str, Enum): 5 | ONE = "one" 6 | THREE = "three" 7 | TWO = "two" 8 | 9 | def __str__(self) -> str: 10 | return str(self.value) 11 | -------------------------------------------------------------------------------- /openapi_python_client/templates/models_init.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains all the data models used in inputs/outputs """ 2 | 3 | {% for import in imports | sort %} 4 | {{ import }} 5 | {% endfor %} 6 | 7 | {% if imports %} 8 | __all__ = ( 9 | {% for all in alls | sort %} 10 | "{{ all }}", 11 | {% endfor %} 12 | ) 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /openapi_python_client/templates/pyproject.toml.jinja: -------------------------------------------------------------------------------- 1 | {% if meta == "poetry" %} 2 | {% include "pyproject_poetry.toml.jinja" %} 3 | {% elif meta == "pdm" %} 4 | {% include "pyproject_pdm.toml.jinja" %} 5 | {% elif meta == "uv" %} 6 | {% include "pyproject_uv.toml.jinja" %} 7 | {% endif %} 8 | 9 | {% include "pyproject_ruff.toml.jinja" %} 10 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | 14 | # mypy 15 | .mypy_cache/ 16 | .dmypy.json 17 | dmypy.json 18 | 19 | # JetBrains 20 | .idea/ 21 | 22 | /coverage.xml 23 | /.coverage 24 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains all the data models used in inputs/outputs""" 2 | 3 | from .post_const_path_body import PostConstPathBody 4 | from .post_prefix_items_body import PostPrefixItemsBody 5 | 6 | __all__ = ( 7 | "PostConstPathBody", 8 | "PostPrefixItemsBody", 9 | ) 10 | -------------------------------------------------------------------------------- /openapi_python_client/templates/.gitignore.jinja: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | 14 | # mypy 15 | .mypy_cache/ 16 | .dmypy.json 17 | dmypy.json 18 | 19 | # JetBrains 20 | .idea/ 21 | 22 | /coverage.xml 23 | /.coverage 24 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | 14 | # mypy 15 | .mypy_cache/ 16 | .dmypy.json 17 | dmypy.json 18 | 19 | # JetBrains 20 | .idea/ 21 | 22 | /coverage.xml 23 | /.coverage 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: GitHub Discussions 4 | url: https://github.com/openapi-generators/openapi-python-client/discussions 5 | about: Request features and improvements here! 6 | - name: Discord 7 | url: https://discord.gg/JaqVvBgwYw 8 | about: Less structured, more casual chat. 9 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag1/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import get_tag_with_number 6 | 7 | 8 | class Tag1Endpoints: 9 | @classmethod 10 | def get_tag_with_number(cls) -> types.ModuleType: 11 | return get_tag_with_number 12 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag2/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import get_tag_with_number 6 | 7 | 8 | class Tag2Endpoints: 9 | @classmethod 10 | def get_tag_with_number(cls) -> types.ModuleType: 11 | return get_tag_with_number 12 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .pytest_cache/ 6 | 7 | # pyenv 8 | .python-version 9 | 10 | # Environments 11 | .env 12 | .venv 13 | 14 | # mypy 15 | .mypy_cache/ 16 | .dmypy.json 17 | dmypy.json 18 | 19 | # JetBrains 20 | .idea/ 21 | 22 | /coverage.xml 23 | /.coverage 24 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains all the data models used in inputs/outputs""" 2 | 3 | from .model_with_description import ModelWithDescription 4 | from .model_with_no_description import ModelWithNoDescription 5 | 6 | __all__ = ( 7 | "ModelWithDescription", 8 | "ModelWithNoDescription", 9 | ) 10 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_date_property/date_property_template.py.jinja: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from dateutil.parser import isoparse 4 | {% from "property_templates/date_property.py.jinja" import transform, construct %} 5 | some_source = date(2020, 10, 12) 6 | {{ transform(property, "some_source", "some_destination") }} 7 | {{ construct(property, "some_destination") }} 8 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_datetime_property/datetime_property_template.py.jinja: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from dateutil.parser import isoparse 4 | {% from "property_templates/datetime_property.py.jinja" import transform, construct %} 5 | some_source = date(2020, 10, 12) 6 | {{ transform(property, "some_source", "some_destination") }} 7 | {{ construct(property, "some_destination") }} 8 | -------------------------------------------------------------------------------- /openapi_python_client/templates/helpers.jinja: -------------------------------------------------------------------------------- 1 | {% macro safe_docstring(content, omit_if_empty=False) %} 2 | {# This macro returns the provided content as a docstring, set to a raw string if it contains a backslash #} 3 | {% if (not omit_if_empty) or (content | trim) %} 4 | {% if '\\' in content -%} 5 | r""" {{ content }} """ 6 | {%- else -%} 7 | """ {{ content }} """ 8 | {%- endif -%} 9 | {% endif %} 10 | {% endmacro %} -------------------------------------------------------------------------------- /end_to_end_tests/documents_with_errors/bad-status-code.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: "There's something wrong with me" 4 | version: "0.1.0" 5 | paths: 6 | "/": 7 | get: 8 | responses: 9 | "abcdef": 10 | description: "Successful Response" 11 | content: 12 | "application/json": 13 | schema: 14 | const: "Why have a fixed response? I dunno" 15 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/config/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import content_type_override 6 | 7 | 8 | class ConfigEndpoints: 9 | @classmethod 10 | def content_type_override(cls) -> types.ModuleType: 11 | """ 12 | Content Type Override 13 | """ 14 | return content_type_override 15 | -------------------------------------------------------------------------------- /end_to_end_tests/config.yml: -------------------------------------------------------------------------------- 1 | class_overrides: 2 | _ABCResponse: 3 | class_name: ABCResponse 4 | module_name: abc_response 5 | AnEnumValueItem: 6 | class_name: AnEnumValue 7 | module_name: an_enum_value 8 | NestedListOfEnumsItemItem: 9 | class_name: AnEnumValue 10 | module_name: an_enum_value 11 | field_prefix: attr_ 12 | content_type_overrides: 13 | openapi/python/client: application/json 14 | generate_all_tags: true 15 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/defaults/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import defaults_tests_defaults_post 6 | 7 | 8 | class DefaultsEndpoints: 9 | @classmethod 10 | def defaults_tests_defaults_post(cls) -> types.ModuleType: 11 | """ 12 | Defaults 13 | """ 14 | return defaults_tests_defaults_post 15 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | AnEnum = Literal["FIRST_VALUE", "SECOND_VALUE"] 4 | 5 | AN_ENUM_VALUES: set[AnEnum] = { 6 | "FIRST_VALUE", 7 | "SECOND_VALUE", 8 | } 9 | 10 | 11 | def check_an_enum(value: str) -> AnEnum: 12 | if value in AN_ENUM_VALUES: 13 | return cast(AnEnum, value) 14 | raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_ENUM_VALUES!r}") 15 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_int_enum.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | AnIntEnum = Literal[-1, 1, 2] 4 | 5 | AN_INT_ENUM_VALUES: set[AnIntEnum] = { 6 | -1, 7 | 1, 8 | 2, 9 | } 10 | 11 | 12 | def check_an_int_enum(value: int) -> AnIntEnum: 13 | if value in AN_INT_ENUM_VALUES: 14 | return cast(AnIntEnum, value) 15 | raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_INT_ENUM_VALUES!r}") 16 | -------------------------------------------------------------------------------- /tests/test_templates/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jinja2 import Environment, PackageLoader 3 | 4 | 5 | @pytest.fixture(scope="session") 6 | def env() -> Environment: 7 | from openapi_python_client import utils 8 | 9 | TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case} 10 | env = Environment(loader=PackageLoader("openapi_python_client"), trim_blocks=True, lstrip_blocks=True) 11 | env.filters.update(TEMPLATE_FILTERS) 12 | return env 13 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/helpers.jinja: -------------------------------------------------------------------------------- 1 | {% macro guarded_statement(property, source, statement) %} 2 | {# If the property can be UNSET or None, this macro returns the provided statement guarded by an if which will check 3 | for those invalid values. Otherwise, it returns the statement unmodified. #} 4 | {% if property.required %} 5 | {{ statement }} 6 | {% else %} 7 | if not isinstance({{ source }}, Unset): 8 | {{ statement }} 9 | {% endif %} 10 | {% endmacro %} 11 | -------------------------------------------------------------------------------- /end_to_end_tests/test_custom_templates/api_init.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains methods for accessing the API """ 2 | 3 | {% for tag in endpoint_collections_by_tag.keys() %} 4 | from .{{ tag }} import {{ class_name(tag) }}Endpoints 5 | {% endfor %} 6 | 7 | class {{ class_name(package_name) }}Api: 8 | {% for tag in endpoint_collections_by_tag.keys() %} 9 | @classmethod 10 | def {{ tag }}(cls) -> type[{{ class_name(tag) }}Endpoints]: 11 | return {{ class_name(tag) }}Endpoints 12 | {% endfor %} 13 | -------------------------------------------------------------------------------- /openapi_python_client/templates/pyproject_pdm.toml.jinja: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "{{ project_name }}" 3 | version = "{{ package_version }}" 4 | description = "{{ package_description }}" 5 | authors = [] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "httpx>=0.23.0,<0.29.0", 10 | "attrs>=22.2.0", 11 | "python-dateutil>=2.8.0", 12 | ] 13 | 14 | [tool.pdm] 15 | distribution = true 16 | 17 | [build-system] 18 | requires = ["pdm-backend"] 19 | build-backend = "pdm.backend" 20 | -------------------------------------------------------------------------------- /end_to_end_tests/documents_with_errors/missing-body-ref.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: "Trying to use a request body ref that does not exist" 4 | version: "0.1.0" 5 | paths: 6 | /: 7 | post: 8 | requestBody: 9 | $ref: "#/components/requestBodies/body" 10 | responses: 11 | "200": 12 | description: "Successful Response" 13 | content: 14 | "application/json": 15 | schema: 16 | const: "Why have a fixed response? I dunno" -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameter_references/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import get_parameter_references_path_param 6 | 7 | 8 | class ParameterReferencesEndpoints: 9 | @classmethod 10 | def get_parameter_references_path_param(cls) -> types.ModuleType: 11 | """ 12 | Test different types of parameter references 13 | """ 14 | return get_parameter_references_path_param 15 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/different_enum.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | DifferentEnum = Literal["DIFFERENT", "OTHER"] 4 | 5 | DIFFERENT_ENUM_VALUES: set[DifferentEnum] = { 6 | "DIFFERENT", 7 | "OTHER", 8 | } 9 | 10 | 11 | def check_different_enum(value: str) -> DifferentEnum: 12 | if value in DIFFERENT_ENUM_VALUES: 13 | return cast(DifferentEnum, value) 14 | raise TypeError(f"Unexpected value {value!r}. Expected one of {DIFFERENT_ENUM_VALUES!r}") 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest release is currently supported, we will not be backporting fixes as long as this project is in the unstable 0.x versioning. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you've discovered a vulnerability in this project, please report it to Dylan Anthony at contact@dylananthony.com. I will create an advisory, add you to the discussion, and credit you with discovery. 10 | 11 | It's better not to create an issue in the repository unless it's already actively being exploited. 12 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/location/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import get_location_header_types, get_location_query_optionality 6 | 7 | 8 | class LocationEndpoints: 9 | @classmethod 10 | def get_location_query_optionality(cls) -> types.ModuleType: 11 | return get_location_query_optionality 12 | 13 | @classmethod 14 | def get_location_header_types(cls) -> types.ModuleType: 15 | return get_location_header_types 16 | -------------------------------------------------------------------------------- /openapi_python_client/schema/data_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class DataType(str, Enum): 5 | """The data type of a schema is defined by the type keyword 6 | 7 | References: 8 | - https://swagger.io/docs/specification/data-models/data-types/ 9 | - https://json-schema.org/draft/2020-12/json-schema-validation.html#name-type 10 | """ 11 | 12 | STRING = "string" 13 | NUMBER = "number" 14 | INTEGER = "integer" 15 | BOOLEAN = "boolean" 16 | ARRAY = "array" 17 | OBJECT = "object" 18 | NULL = "null" 19 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum_with_null.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | AnEnumWithNull = Literal["FIRST_VALUE", "SECOND_VALUE"] 4 | 5 | AN_ENUM_WITH_NULL_VALUES: set[AnEnumWithNull] = { 6 | "FIRST_VALUE", 7 | "SECOND_VALUE", 8 | } 9 | 10 | 11 | def check_an_enum_with_null(value: str) -> AnEnumWithNull: 12 | if value in AN_ENUM_WITH_NULL_VALUES: 13 | return cast(AnEnumWithNull, value) 14 | raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_ENUM_WITH_NULL_VALUES!r}") 15 | -------------------------------------------------------------------------------- /openapi_python_client/templates/pyproject_uv.toml.jinja: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "{{ project_name }}" 3 | version = "{{ package_version }}" 4 | description = "{{ package_description }}" 5 | authors = [] 6 | requires-python = ">=3.10" 7 | readme = "README.md" 8 | dependencies = [ 9 | "httpx>=0.23.0,<0.29.0", 10 | "attrs>=22.2.0", 11 | "python-dateutil>=2.8.0,<3", 12 | ] 13 | 14 | [tool.uv.build-backend] 15 | module-name = "{{ package_name }}" 16 | module-root = "" 17 | 18 | [build-system] 19 | requires = ["uv_build>=0.9.0,<0.10.0"] 20 | build-backend = "uv_build" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pdm-python 2 | __pycache__/ 3 | build/ 4 | dist/ 5 | *.egg-info/ 6 | .pytest_cache/ 7 | .ruff_cache 8 | 9 | # macOS 10 | .DS_Store 11 | 12 | # pyenv 13 | .python-version 14 | 15 | # Environments 16 | .env 17 | .venv 18 | 19 | # mypy 20 | .mypy_cache/ 21 | .dmypy.json 22 | dmypy.json 23 | 24 | # JetBrains 25 | .idea/ 26 | 27 | # Visual Studio Code 28 | .vscode/ 29 | 30 | test-reports/ 31 | 32 | /coverage.xml 33 | /.coverage 34 | htmlcov/ 35 | 36 | # Generated end to end test data 37 | my-test-api-client/ 38 | custom-e2e/ 39 | 3-1-features-client 40 | tests/tmp -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Testing that we can access model-related information via Jinja variables. 2 | 3 | # To avoid having to update this file in the golden record every time the test specs are changed, 4 | # we won't include all the classes in this output - we'll just look for one of them. 5 | 6 | # Using "alls" 7 | # AModel 8 | 9 | # Using "imports" 10 | # from .a_model import AModel 11 | 12 | # Using "openapi.models" 13 | # AModel (a_model) 14 | 15 | # Using "openapi.enums" 16 | # AnEnum (an_enum) 17 | -------------------------------------------------------------------------------- /end_to_end_tests/documents_with_errors/optional-path-param.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: "There's something wrong with me" 4 | version: "0.1.0" 5 | paths: 6 | "/{optional}": 7 | get: 8 | parameters: 9 | - in: "path" 10 | name: "optional" 11 | schema: 12 | type: "string" 13 | responses: 14 | "200": 15 | description: "Successful Response" 16 | content: 17 | "application/json": 18 | schema: 19 | const: "Why have a fixed response? I dunno" 20 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_all_of_enum.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | AnAllOfEnum = Literal["a_default", "bar", "foo", "overridden_default"] 4 | 5 | AN_ALL_OF_ENUM_VALUES: set[AnAllOfEnum] = { 6 | "a_default", 7 | "bar", 8 | "foo", 9 | "overridden_default", 10 | } 11 | 12 | 13 | def check_an_all_of_enum(value: str) -> AnAllOfEnum: 14 | if value in AN_ALL_OF_ENUM_VALUES: 15 | return cast(AnAllOfEnum, value) 16 | raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_ALL_OF_ENUM_VALUES!r}") 17 | -------------------------------------------------------------------------------- /end_to_end_tests/documents_with_errors/circular-body-ref.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: "Circular Body Ref" 4 | version: "0.1.0" 5 | paths: 6 | /: 7 | post: 8 | requestBody: 9 | $ref: "#/components/requestBodies/body" 10 | responses: 11 | "200": 12 | description: "Successful Response" 13 | content: 14 | "application/json": 15 | schema: 16 | const: "Why have a fixed response? I dunno" 17 | components: 18 | requestBodies: 19 | body: 20 | $ref: "#/components/requestBodies/body" -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/paths.py: -------------------------------------------------------------------------------- 1 | from .path_item import PathItem 2 | 3 | Paths = dict[str, PathItem] 4 | """ 5 | Holds the relative paths to the individual endpoints and their operations. 6 | The path is appended to the URL from the [`Server Object`](#serverObject) in order to construct the full URL. 7 | 8 | The Paths MAY be empty, due to [ACL constraints](#securityFiltering). 9 | 10 | References: 11 | - https://swagger.io/docs/specification/paths-and-operations/ 12 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathsObject 13 | """ 14 | -------------------------------------------------------------------------------- /openapi_python_client/templates/pyproject_poetry.toml.jinja: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "{{ project_name }}" 3 | version = "{{ package_version }}" 4 | description = "{{ package_description }}" 5 | authors = [] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "{{ package_name }}" }, 9 | ] 10 | include = ["{{ package_name }}/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | httpx = ">=0.23.0,<0.29.0" 15 | attrs = ">=22.2.0" 16 | python-dateutil = "^2.8.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=2.0.0,<3.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /end_to_end_tests/metadata_snapshots/pdm.pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "test-3-1-features-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing Test 3.1 Features" 5 | authors = [] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "httpx>=0.23.0,<0.29.0", 10 | "attrs>=22.2.0", 11 | "python-dateutil>=2.8.0", 12 | ] 13 | 14 | [tool.pdm] 15 | distribution = true 16 | 17 | [build-system] 18 | requires = ["pdm-backend"] 19 | build-backend = "pdm.backend" 20 | 21 | [tool.ruff] 22 | line-length = 120 23 | 24 | [tool.ruff.lint] 25 | select = ["F", "I", "UP"] 26 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/server_variable.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class ServerVariable(BaseModel): 5 | """An object representing a Server Variable for server URL template substitution. 6 | 7 | References: 8 | - https://swagger.io/docs/specification/api-host-and-base-path/ 9 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverVariableObject 10 | """ 11 | 12 | enum: list[str] | None = None 13 | default: str 14 | description: str | None = None 15 | model_config = ConfigDict(extra="allow") 16 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_int_enum_header.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | GetUserListIntEnumHeader = Literal[1, 2, 3] 4 | 5 | GET_USER_LIST_INT_ENUM_HEADER_VALUES: set[GetUserListIntEnumHeader] = { 6 | 1, 7 | 2, 8 | 3, 9 | } 10 | 11 | 12 | def check_get_user_list_int_enum_header(value: int) -> GetUserListIntEnumHeader: 13 | if value in GET_USER_LIST_INT_ENUM_HEADER_VALUES: 14 | return cast(GetUserListIntEnumHeader, value) 15 | raise TypeError(f"Unexpected value {value!r}. Expected one of {GET_USER_LIST_INT_ENUM_HEADER_VALUES!r}") 16 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/license.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class License(BaseModel): 5 | """ 6 | License information for the exposed API. 7 | 8 | References: 9 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject 10 | """ 11 | 12 | name: str 13 | url: str | None = None 14 | model_config = ConfigDict( 15 | extra="allow", 16 | json_schema_extra={ 17 | "examples": [{"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"}] 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_parser/test_properties/test_file.py: -------------------------------------------------------------------------------- 1 | from openapi_python_client.parser.errors import PropertyError 2 | from openapi_python_client.parser.properties import FileProperty 3 | 4 | 5 | def test_no_default_allowed(): 6 | # currently this is testing an unused code path: 7 | # https://github.com/openapi-generators/openapi-python-client/issues/1162 8 | err = FileProperty.build( 9 | default="not none", 10 | description=None, 11 | example=None, 12 | required=False, 13 | python_name="not_none", 14 | name="not_none", 15 | ) 16 | 17 | assert isinstance(err, PropertyError) 18 | -------------------------------------------------------------------------------- /openapi_python_client/templates/errors.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains shared errors types that can be raised from API functions """ 2 | 3 | class UnexpectedStatus(Exception): 4 | """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" 5 | 6 | def __init__(self, status_code: int, content: bytes): 7 | self.status_code = status_code 8 | self.content = content 9 | 10 | super().__init__( 11 | f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" 12 | ) 13 | 14 | __all__ = ["UnexpectedStatus"] 15 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/errors.py: -------------------------------------------------------------------------------- 1 | """Contains shared errors types that can be raised from API functions""" 2 | 3 | 4 | class UnexpectedStatus(Exception): 5 | """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" 6 | 7 | def __init__(self, status_code: int, content: bytes): 8 | self.status_code = status_code 9 | self.content = content 10 | 11 | super().__init__( 12 | f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" 13 | ) 14 | 15 | 16 | __all__ = ["UnexpectedStatus"] 17 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class ExternalDocumentation(BaseModel): 5 | """Allows referencing an external resource for extended documentation. 6 | 7 | References: 8 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#externalDocumentationObject 9 | """ 10 | 11 | description: str | None = None 12 | url: str 13 | model_config = ConfigDict( 14 | extra="allow", 15 | json_schema_extra={"examples": [{"description": "Find more info here", "url": "https://example.com"}]}, 16 | ) 17 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/errors.py: -------------------------------------------------------------------------------- 1 | """Contains shared errors types that can be raised from API functions""" 2 | 3 | 4 | class UnexpectedStatus(Exception): 5 | """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" 6 | 7 | def __init__(self, status_code: int, content: bytes): 8 | self.status_code = status_code 9 | self.content = content 10 | 11 | super().__init__( 12 | f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" 13 | ) 14 | 15 | 16 | __all__ = ["UnexpectedStatus"] 17 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/const_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct(property, source) %} 2 | {{ property.python_name }} = cast({{ property.get_type_string() }} , {{ source }}) 3 | if {{ property.python_name }} != {{ property.value.python_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}: 4 | raise ValueError(f"{{ property.name }} must match const {{ property.value.python_code }}, got '{{'{' + property.python_name + '}' }}'") 5 | {%- endmacro %} 6 | 7 | {% macro multipart(property, source, name) %} 8 | files.append(({{ name }}, (None, {{ source }}, "text/plain"))) 9 | {% endmacro %} 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: [main] 7 | 8 | jobs: 9 | release: 10 | if: github.head_ref == 'knope/release' && github.event.pull_request.merged == true 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v6.0.1 16 | - name: Install Hatchling 17 | run: pip install --upgrade hatchling 18 | - name: Build 19 | run: hatchling build 20 | - name: Push to PyPI 21 | uses: pypa/gh-action-pypi-publish@v1.13.0 22 | with: 23 | attestations: true 24 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/enums/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import bool_enum_tests_bool_enum_post, int_enum_tests_int_enum_post 6 | 7 | 8 | class EnumsEndpoints: 9 | @classmethod 10 | def int_enum_tests_int_enum_post(cls) -> types.ModuleType: 11 | """ 12 | Int Enum 13 | """ 14 | return int_enum_tests_int_enum_post 15 | 16 | @classmethod 17 | def bool_enum_tests_bool_enum_post(cls) -> types.ModuleType: 18 | """ 19 | Bool Enum 20 | """ 21 | return bool_enum_tests_bool_enum_post 22 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import hyphen_in_path, mixed_case, post_naming_property_conflict_with_import 6 | 7 | 8 | class NamingEndpoints: 9 | @classmethod 10 | def post_naming_property_conflict_with_import(cls) -> types.ModuleType: 11 | return post_naming_property_conflict_with_import 12 | 13 | @classmethod 14 | def mixed_case(cls) -> types.ModuleType: 15 | return mixed_case 16 | 17 | @classmethod 18 | def hyphen_in_path(cls) -> types.ModuleType: 19 | return hyphen_in_path 20 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/errors.py: -------------------------------------------------------------------------------- 1 | """Contains shared errors types that can be raised from API functions""" 2 | 3 | 4 | class UnexpectedStatus(Exception): 5 | """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" 6 | 7 | def __init__(self, status_code: int, content: bytes): 8 | self.status_code = status_code 9 | self.content = content 10 | 11 | super().__init__( 12 | f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" 13 | ) 14 | 15 | 16 | __all__ = ["UnexpectedStatus"] 17 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/errors.py: -------------------------------------------------------------------------------- 1 | """Contains shared errors types that can be raised from API functions""" 2 | 3 | 4 | class UnexpectedStatus(Exception): 5 | """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" 6 | 7 | def __init__(self, status_code: int, content: bytes): 8 | self.status_code = status_code 9 | self.content = content 10 | 11 | super().__init__( 12 | f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" 13 | ) 14 | 15 | 16 | __all__ = ["UnexpectedStatus"] 17 | -------------------------------------------------------------------------------- /openapi_python_client/schema/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "DataType", 3 | "MediaType", 4 | "OpenAPI", 5 | "Operation", 6 | "Parameter", 7 | "Parameter", 8 | "ParameterLocation", 9 | "PathItem", 10 | "Reference", 11 | "RequestBody", 12 | "Response", 13 | "Responses", 14 | "Schema", 15 | ] 16 | 17 | 18 | from .data_type import DataType 19 | from .openapi_schema_pydantic import ( 20 | MediaType, 21 | OpenAPI, 22 | Operation, 23 | Parameter, 24 | PathItem, 25 | Reference, 26 | RequestBody, 27 | Response, 28 | Responses, 29 | Schema, 30 | ) 31 | from .parameter_location import ParameterLocation 32 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_string_enum_header.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | GetUserListStringEnumHeader = Literal["one", "three", "two"] 4 | 5 | GET_USER_LIST_STRING_ENUM_HEADER_VALUES: set[GetUserListStringEnumHeader] = { 6 | "one", 7 | "three", 8 | "two", 9 | } 10 | 11 | 12 | def check_get_user_list_string_enum_header(value: str) -> GetUserListStringEnumHeader: 13 | if value in GET_USER_LIST_STRING_ENUM_HEADER_VALUES: 14 | return cast(GetUserListStringEnumHeader, value) 15 | raise TypeError(f"Unexpected value {value!r}. Expected one of {GET_USER_LIST_STRING_ENUM_HEADER_VALUES!r}") 16 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/callback.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: # pragma: no cover 4 | from .path_item import PathItem 5 | else: 6 | PathItem = "PathItem" 7 | 8 | Callback = dict[str, PathItem] 9 | """ 10 | A map of possible out-of band callbacks related to the parent operation. 11 | Each value in the map is a [Path Item Object](#pathItemObject) 12 | that describes a set of requests that may be initiated by the API provider and the expected responses. 13 | The key value used to identify the path item object is an expression, evaluated at runtime, 14 | that identifies a URL to use for the callback operation. 15 | """ 16 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/errors.py: -------------------------------------------------------------------------------- 1 | """Contains shared errors types that can be raised from API functions""" 2 | 3 | 4 | class UnexpectedStatus(Exception): 5 | """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" 6 | 7 | def __init__(self, status_code: int, content: bytes): 8 | self.status_code = status_code 9 | self.content = content 10 | 11 | super().__init__( 12 | f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" 13 | ) 14 | 15 | 16 | __all__ = ["UnexpectedStatus"] 17 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "my-test-api-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing My Test API" 5 | authors = [] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "my_test_api_client" }, 9 | ] 10 | include = ["my_test_api_client/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | httpx = ">=0.23.0,<0.29.0" 15 | attrs = ">=22.2.0" 16 | python-dateutil = "^2.8.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=2.0.0,<3.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | [tool.ruff] 23 | line-length = 120 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "I", "UP"] 27 | -------------------------------------------------------------------------------- /end_to_end_tests/metadata_snapshots/uv.pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "test-3-1-features-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing Test 3.1 Features" 5 | authors = [] 6 | requires-python = ">=3.10" 7 | readme = "README.md" 8 | dependencies = [ 9 | "httpx>=0.23.0,<0.29.0", 10 | "attrs>=22.2.0", 11 | "python-dateutil>=2.8.0,<3", 12 | ] 13 | 14 | [tool.uv.build-backend] 15 | module-name = "test_3_1_features_client" 16 | module-root = "" 17 | 18 | [build-system] 19 | requires = ["uv_build>=0.9.0,<0.10.0"] 20 | build-backend = "uv_build" 21 | 22 | [tool.ruff] 23 | line-length = 120 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "I", "UP"] 27 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains all the data models used in inputs/outputs""" 2 | 3 | from .an_object import AnObject 4 | from .file import File 5 | from .post_body_multipart_body import PostBodyMultipartBody 6 | from .post_body_multipart_response_200 import PostBodyMultipartResponse200 7 | from .post_parameters_header_response_200 import PostParametersHeaderResponse200 8 | from .problem import Problem 9 | from .public_error import PublicError 10 | 11 | __all__ = ( 12 | "AnObject", 13 | "File", 14 | "PostBodyMultipartBody", 15 | "PostBodyMultipartResponse200", 16 | "PostParametersHeaderResponse200", 17 | "Problem", 18 | "PublicError", 19 | ) 20 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/property_macros.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_template(construct_function, property, source) %} 2 | {% if property.required %} 3 | {{ property.python_name }} = {{ construct_function(property, source) }} 4 | {% else %}{# Must be non-required #} 5 | _{{ property.python_name }} = {{ source }} 6 | {{ property.python_name }}: {{ property.get_type_string() }} 7 | {% if not property.required %} 8 | if isinstance(_{{ property.python_name }}, Unset): 9 | {{ property.python_name }} = UNSET 10 | {% endif %} 11 | else: 12 | {{ property.python_name }} = {{ construct_function(property, "_" + property.python_name) }} 13 | {% endif %} 14 | {% endmacro %} 15 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "my-enum-api-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing My Enum API" 5 | authors = [] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "my_enum_api_client" }, 9 | ] 10 | include = ["my_enum_api_client/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | httpx = ">=0.23.0,<0.29.0" 15 | attrs = ">=22.2.0" 16 | python-dateutil = "^2.8.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=2.0.0,<3.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | [tool.ruff] 23 | line-length = 120 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "I", "UP"] 27 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | 8 | T = TypeVar("T", bound="ModelWithNoProperties") 9 | 10 | 11 | @_attrs_define 12 | class ModelWithNoProperties: 13 | """ """ 14 | 15 | def to_dict(self) -> dict[str, Any]: 16 | field_dict: dict[str, Any] = {} 17 | 18 | return field_dict 19 | 20 | @classmethod 21 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 22 | model_with_no_properties = cls() 23 | 24 | return model_with_no_properties 25 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "my-test-api-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing My Test API" 5 | authors = [] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "my_test_api_client" }, 9 | ] 10 | include = ["my_test_api_client/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | httpx = ">=0.23.0,<0.29.0" 15 | attrs = ">=22.2.0" 16 | python-dateutil = "^2.8.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=2.0.0,<3.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | [tool.ruff] 23 | line-length = 120 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "I", "UP"] 27 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | from .oauth_flow import OAuthFlow 4 | 5 | 6 | class OAuthFlows(BaseModel): 7 | """ 8 | Allows configuration of the supported OAuth Flows. 9 | 10 | References: 11 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject 12 | - https://swagger.io/docs/specification/authentication/oauth2/ 13 | """ 14 | 15 | implicit: OAuthFlow | None = None 16 | password: OAuthFlow | None = None 17 | clientCredentials: OAuthFlow | None = None 18 | authorizationCode: OAuthFlow | None = None 19 | model_config = ConfigDict(extra="allow") 20 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "test-3-1-features-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing Test 3.1 Features" 5 | authors = [] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "test_3_1_features_client" }, 9 | ] 10 | include = ["test_3_1_features_client/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | httpx = ">=0.23.0,<0.29.0" 15 | attrs = ">=22.2.0" 16 | python-dateutil = "^2.8.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=2.0.0,<3.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | [tool.ruff] 23 | line-length = 120 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "I", "UP"] 27 | -------------------------------------------------------------------------------- /openapi_python_client/templates/setup.py.jinja: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from setuptools import find_packages, setup 4 | 5 | here = pathlib.Path(__file__).parent.resolve() 6 | long_description = (here / "README.md").read_text(encoding="utf-8") 7 | 8 | setup( 9 | name="{{ project_name }}", 10 | version="{{ package_version }}", 11 | description="{{ package_description }}", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | packages=find_packages(), 15 | python_requires=">=3.10, <4", 16 | install_requires=["httpx >= 0.23.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], 17 | package_data={"{{ package_name }}": ["py.typed"]}, 18 | ) 19 | -------------------------------------------------------------------------------- /end_to_end_tests/metadata_snapshots/poetry.pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "test-3-1-features-client" 3 | version = "0.1.0" 4 | description = "A client library for accessing Test 3.1 Features" 5 | authors = [] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "test_3_1_features_client" }, 9 | ] 10 | include = ["test_3_1_features_client/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.10" 14 | httpx = ">=0.23.0,<0.29.0" 15 | attrs = ">=22.2.0" 16 | python-dateutil = "^2.8.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=2.0.0,<3.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | [tool.ruff] 23 | line-length = 120 24 | 25 | [tool.ruff.lint] 26 | select = ["F", "I", "UP"] 27 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/contact.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class Contact(BaseModel): 5 | """ 6 | Contact information for the exposed API. 7 | 8 | See Also: 9 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject 10 | """ 11 | 12 | name: str | None = None 13 | url: str | None = None 14 | email: str | None = None 15 | model_config = ConfigDict( 16 | extra="allow", 17 | json_schema_extra={ 18 | "examples": [ 19 | {"name": "API Support", "url": "http://www.example.com/support", "email": "support@example.com"} 20 | ] 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /openapi_python_client/templates/literal_enum.py.jinja: -------------------------------------------------------------------------------- 1 | from typing import Literal, cast 2 | 3 | {{ enum.class_info.name }} = Literal{{ "%r" | format(enum.values|list|sort) }} 4 | 5 | {{ enum.get_class_name_snake_case() | upper }}_VALUES: set[{{ enum.class_info.name }}] = { {% for v in enum.values|list|sort %}{{"%r"|format(v)}}, {% endfor %} } 6 | 7 | def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() }}) -> {{ enum.class_info.name}}: 8 | if value in {{ enum.get_class_name_snake_case() | upper }}_VALUES: 9 | return cast({{enum.class_info.name}}, value) 10 | raise TypeError(f"Unexpected value {value!r}. Expected one of {{"{"}}{{ enum.get_class_name_snake_case() | upper }}_VALUES!r}") 11 | -------------------------------------------------------------------------------- /end_to_end_tests/metadata_snapshots/setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from setuptools import find_packages, setup 4 | 5 | here = pathlib.Path(__file__).parent.resolve() 6 | long_description = (here / "README.md").read_text(encoding="utf-8") 7 | 8 | setup( 9 | name="test-3-1-features-client", 10 | version="0.1.0", 11 | description="A client library for accessing Test 3.1 Features", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | packages=find_packages(), 15 | python_requires=">=3.10, <4", 16 | install_requires=["httpx >= 0.23.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], 17 | package_data={"test_3_1_features_client": ["py.typed"]}, 18 | ) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: Missing OpenAPI functionality are feature requests, not bugs! 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. If this used to work, when did it stop working? 12 | 13 | **OpenAPI Spec File** 14 | A link to an OpenAPI document which produces this issue. Ideally, write a minimal reproduction only containing the problematic pieces. 15 | 16 | **Desktop (please complete the following information):** 17 | - OS: [e.g. macOS 10.15.1] 18 | - Python Version: [e.g. 3.8.0] 19 | - openapi-python-client version [e.g. 0.1.0] 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":semanticCommitTypeAll(chore)" 5 | ], 6 | "rangeStrategy": "widen", 7 | "lockFileMaintenance": { "enabled": true, "automerge": true }, 8 | "regexManagers": [ 9 | { 10 | "fileMatch": [ 11 | "release.*\\.yml", 12 | "prerelease.yml" 13 | ], 14 | "matchStrings": [ 15 | "version:\\s*(?.*)" 16 | ], 17 | "depNameTemplate": "knope", 18 | "datasourceTemplate": "crate", 19 | "versioningTemplate": "semver" 20 | } 21 | ], 22 | "packageRules": [ 23 | { 24 | "packagePatterns": [ 25 | "^knope$" 26 | ], 27 | "groupName": "knope", 28 | "rangeStrategy": "pin" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: "Circular Body Ref" 4 | version: "0.1.0" 5 | paths: 6 | /: 7 | post: 8 | parameters: 9 | - name: id 10 | in: query 11 | required: false 12 | schema: 13 | type: string 14 | format: uuid 15 | default: "notauuid" 16 | responses: 17 | "200": 18 | description: "Successful Response" 19 | put: 20 | parameters: 21 | - name: another_id 22 | in: query 23 | required: false 24 | schema: 25 | type: string 26 | format: uuid 27 | default: 3 28 | responses: 29 | "200": 30 | description: "Successful Response" -------------------------------------------------------------------------------- /openapi_python_client/schema/parameter_location.py: -------------------------------------------------------------------------------- 1 | # Python 3.11 has StrEnum but breaks the old `str, Enum` hack. 2 | # Unless this gets fixed, we need to have two implementations :( 3 | import sys 4 | 5 | if sys.version_info >= (3, 11): 6 | from enum import StrEnum 7 | 8 | class ParameterLocation(StrEnum): 9 | """The places Parameters can be put when calling an Endpoint""" 10 | 11 | QUERY = "query" 12 | PATH = "path" 13 | HEADER = "header" 14 | COOKIE = "cookie" 15 | 16 | else: 17 | from enum import Enum 18 | 19 | class ParameterLocation(str, Enum): 20 | """The places Parameters can be put when calling an Endpoint""" 21 | 22 | QUERY = "query" 23 | PATH = "path" 24 | HEADER = "header" 25 | COOKIE = "cookie" 26 | -------------------------------------------------------------------------------- /end_to_end_tests/test_custom_templates/endpoint_init.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains methods for accessing the API Endpoints """ 2 | 3 | import types 4 | {% for endpoint in endpoint_collection.endpoints %} 5 | from . import {{ python_identifier(endpoint.name) }} 6 | {% endfor %} 7 | 8 | class {{ class_name(endpoint_collection.tag) }}Endpoints: 9 | 10 | {% for endpoint in endpoint_collection.endpoints %} 11 | 12 | @classmethod 13 | def {{ python_identifier(endpoint.name) }}(cls) -> types.ModuleType: 14 | {% if endpoint.description %} 15 | """ 16 | {{ endpoint.description }} 17 | """ 18 | {% elif endpoint.summary %} 19 | """ 20 | {{ endpoint.summary }} 21 | """ 22 | {% endif %} 23 | return {{ python_identifier(endpoint.name) }} 24 | {% endfor %} 25 | -------------------------------------------------------------------------------- /end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture 4 | 5 | 6 | @with_generated_client_fixture( 7 | """ 8 | components: 9 | schemas: 10 | ArrayWithNoItems: 11 | type: array 12 | ArrayWithInvalidItemsRef: 13 | type: array 14 | items: 15 | $ref: "#/components/schemas/DoesntExist" 16 | """ 17 | ) 18 | class TestArrayInvalidSchemas: 19 | def test_no_items(self, generated_client): 20 | assert_bad_schema(generated_client, "ArrayWithNoItems", "must have items or prefixItems defined") 21 | 22 | def test_invalid_items_ref(self, generated_client): 23 | assert_bad_schema(generated_client, "ArrayWithInvalidItemsRef", "invalid data in items of array") 24 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains all the data models used in inputs/outputs""" 2 | 3 | from .a_model import AModel 4 | from .an_all_of_enum import AnAllOfEnum 5 | from .an_enum import AnEnum 6 | from .an_enum_with_null import AnEnumWithNull 7 | from .an_int_enum import AnIntEnum 8 | from .different_enum import DifferentEnum 9 | from .get_user_list_int_enum_header import GetUserListIntEnumHeader 10 | from .get_user_list_string_enum_header import GetUserListStringEnumHeader 11 | from .post_user_list_body import PostUserListBody 12 | 13 | __all__ = ( 14 | "AModel", 15 | "AnAllOfEnum", 16 | "AnEnum", 17 | "AnEnumWithNull", 18 | "AnIntEnum", 19 | "DifferentEnum", 20 | "GetUserListIntEnumHeader", 21 | "GetUserListStringEnumHeader", 22 | "PostUserListBody", 23 | ) 24 | -------------------------------------------------------------------------------- /integration-tests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "integration-tests" 3 | version = "0.1.0" 4 | description = "A client library for accessing OpenAPI Test Server" 5 | authors = [] 6 | readme = "README.md" 7 | requires-python = ">=3.10,<4.0" 8 | dependencies = [ 9 | "httpx>=0.23.0,<0.29.0", 10 | "attrs>=22.2.0", 11 | "python-dateutil>=2.8.0", 12 | ] 13 | 14 | [tool.pdm] 15 | distribution = true 16 | 17 | [tool.pdm.dev-dependencies] 18 | dev = [ 19 | "pytest>8", 20 | "mypy>=1.13", 21 | "pytest-asyncio>=0.23.5", 22 | "types-python-dateutil>=2.9", 23 | ] 24 | 25 | [build-system] 26 | requires = ["pdm-backend"] 27 | build-backend = "pdm.backend" 28 | 29 | [tool.ruff] 30 | line-length = 120 31 | 32 | [tool.ruff.lint] 33 | select = ["F", "I"] 34 | 35 | [tool.mypy] 36 | # Just to get mypy to _not_ look at the parent directory's config 37 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/tag.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | from .external_documentation import ExternalDocumentation 4 | 5 | 6 | class Tag(BaseModel): 7 | """ 8 | Adds metadata to a single tag that is used by the [Operation Object](#operationObject). 9 | It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. 10 | 11 | References: 12 | - https://swagger.io/docs/specification/paths-and-operations/ 13 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject 14 | """ 15 | 16 | name: str 17 | description: str | None = None 18 | externalDocs: ExternalDocumentation | None = None 19 | model_config = ConfigDict( 20 | extra="allow", json_schema_extra={"examples": [{"name": "pet", "description": "Pets operations"}]} 21 | ) 22 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/responses.py: -------------------------------------------------------------------------------- 1 | from .reference import ReferenceOr 2 | from .response import Response 3 | 4 | Responses = dict[str, ReferenceOr[Response]] 5 | """ 6 | A container for the expected responses of an operation. 7 | The container maps a HTTP response code to the expected response. 8 | 9 | The documentation is not necessarily expected to cover all possible HTTP response codes 10 | because they may not be known in advance. 11 | However, documentation is expected to cover a successful operation response and any known errors. 12 | 13 | The `default` MAY be used as a default response object for all HTTP codes 14 | that are not covered individually by the specification. 15 | 16 | The `Responses Object` MUST contain at least one response code, and it 17 | SHOULD be the response for a successful operation call. 18 | 19 | References: 20 | - https://swagger.io/docs/specification/describing-responses/ 21 | """ 22 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py: -------------------------------------------------------------------------------- 1 | SecurityRequirement = dict[str, list[str]] 2 | """ 3 | Lists the required security schemes to execute this operation. 4 | The name used for each property MUST correspond to a security scheme declared in the 5 | [Security Schemes](#componentsSecuritySchemes) under the [Components Object](#componentsObject). 6 | 7 | Security Requirement Objects that contain multiple schemes require that 8 | all schemes MUST be satisfied for a request to be authorized. 9 | This enables support for scenarios where multiple query parameters or HTTP headers 10 | are required to convey security information. 11 | 12 | When a list of Security Requirement Objects is defined on the 13 | [OpenAPI Object](#oasObject) or [Operation Object](#operationObject), 14 | only one of the Security Requirement Objects in the list needs to be satisfied to authorize the request. 15 | 16 | References: 17 | - https://swagger.io/docs/specification/authentication/ 18 | """ 19 | -------------------------------------------------------------------------------- /tests/test_schema/test_data_type.py: -------------------------------------------------------------------------------- 1 | import pydantic 2 | import pytest 3 | 4 | import openapi_python_client.schema as oai 5 | 6 | 7 | class TestDataType: 8 | def test_schema_bad_types(self): 9 | with pytest.raises(pydantic.ValidationError): 10 | oai.Schema(type="bad_type") 11 | 12 | with pytest.raises(pydantic.ValidationError): 13 | oai.Schema(anyOf=[{"type": "garbage"}]) 14 | 15 | with pytest.raises(pydantic.ValidationError): 16 | oai.Schema( 17 | properties={ 18 | "bad": oai.Schema(type="not_real"), 19 | }, 20 | ) 21 | 22 | @pytest.mark.parametrize( 23 | "type_", 24 | ( 25 | "string", 26 | "number", 27 | "integer", 28 | "boolean", 29 | "array", 30 | "object", 31 | ), 32 | ) 33 | def test_schema_happy(self, type_): 34 | assert oai.Schema(type=type_).type == type_ 35 | -------------------------------------------------------------------------------- /end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture 4 | 5 | 6 | @with_generated_client_fixture( 7 | """ 8 | components: 9 | schemas: 10 | MyModel: 11 | type: object 12 | properties: 13 | booleanProp: {"type": "boolean"} 14 | stringProp: {"type": "string"} 15 | numberProp: {"type": "number"} 16 | intProp: {"type": "integer"} 17 | anyObjectProp: {"$ref": "#/components/schemas/AnyObject"} 18 | nullProp: {"type": "null"} 19 | anyProp: {} 20 | AnyObject: 21 | $ref: "#/components/schemas/OtherObject" 22 | OtherObject: 23 | $ref: "#/components/schemas/AnyObject" 24 | 25 | """ 26 | ) 27 | class TestReferenceSchemaProperties: 28 | def test_decode_encode(self, generated_client): 29 | assert "Circular schema references found" in generated_client.generator_result.stderr 30 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import json_like, optional_body, post_bodies_multiple, refs 6 | 7 | 8 | class BodiesEndpoints: 9 | @classmethod 10 | def post_bodies_multiple(cls) -> types.ModuleType: 11 | """ 12 | Test multiple bodies 13 | """ 14 | return post_bodies_multiple 15 | 16 | @classmethod 17 | def json_like(cls) -> types.ModuleType: 18 | """ 19 | A content type that works like json but isn't application/json 20 | """ 21 | return json_like 22 | 23 | @classmethod 24 | def refs(cls) -> types.ModuleType: 25 | """ 26 | Test request body defined via ref 27 | """ 28 | return refs 29 | 30 | @classmethod 31 | def optional_body(cls) -> types.ModuleType: 32 | """ 33 | Test optional request body 34 | """ 35 | return optional_body 36 | -------------------------------------------------------------------------------- /end_to_end_tests/test_custom_templates/models_init.py.jinja: -------------------------------------------------------------------------------- 1 | 2 | # Testing that we can access model-related information via Jinja variables. 3 | 4 | # To avoid having to update this file in the golden record every time the test specs are changed, 5 | # we won't include all the classes in this output - we'll just look for one of them. 6 | 7 | # Using "alls" 8 | {% for name in alls %} 9 | {% if name == "AModel" %} 10 | # {{ name }} 11 | {% endif %} 12 | {% endfor %} 13 | 14 | # Using "imports" 15 | {% for import in imports %} 16 | {% if import.endswith("import AModel") %} 17 | # {{ import }} 18 | {% endif %} 19 | {% endfor %} 20 | 21 | # Using "openapi.models" 22 | {% for model in openapi.models %} 23 | {% if model.class_info.name == "AModel" %} 24 | # {{ model.class_info.name }} ({{ model.class_info.module_name }}) 25 | {% endif %} 26 | {% endfor %} 27 | 28 | # Using "openapi.enums" 29 | {% for enum in openapi.enums %} 30 | {% if enum.class_info.name == "AnEnum" %} 31 | # {{ enum.class_info.name }} ({{ enum.class_info.module_name }}) 32 | {% endif %} 33 | {% endfor %} 34 | -------------------------------------------------------------------------------- /tests/test_parser/test_properties/test_none.py: -------------------------------------------------------------------------------- 1 | from openapi_python_client.parser.errors import PropertyError 2 | from openapi_python_client.parser.properties import NoneProperty 3 | from openapi_python_client.parser.properties.protocol import Value 4 | from openapi_python_client.utils import PythonIdentifier 5 | 6 | 7 | def test_default(): 8 | # currently this is testing an unused code path: 9 | # https://github.com/openapi-generators/openapi-python-client/issues/1162 10 | err = NoneProperty.build( 11 | default="not None", 12 | description=None, 13 | example=None, 14 | required=False, 15 | python_name="not_none", 16 | name="not_none", 17 | ) 18 | 19 | assert isinstance(err, PropertyError) 20 | 21 | 22 | def test_dont_retest_values(): 23 | prop = NoneProperty.build( 24 | default=Value("not None", "not None"), 25 | description=None, 26 | example=None, 27 | required=False, 28 | python_name=PythonIdentifier("not_none", ""), 29 | name="not_none", 30 | ) 31 | 32 | assert isinstance(prop, NoneProperty) 33 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/file_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | File( 3 | payload = BytesIO({{ source }}) 4 | ) 5 | {% endmacro %} 6 | 7 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 8 | 9 | {% macro construct(property, source) %} 10 | {{ construct_template(construct_function, property, source) }} 11 | {% endmacro %} 12 | 13 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, bytes){% endmacro %} 14 | 15 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 16 | {% if property.required %} 17 | {{ destination }} = {{ source }}.to_tuple() 18 | {% else %} 19 | {% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET{% endif +%} 20 | if not isinstance({{ source }}, Unset): 21 | {{ destination }} = {{ source }}.to_tuple() 22 | {% endif %} 23 | {% endmacro %} 24 | 25 | {% macro multipart(property, source, name) %} 26 | files.append(({{ name }}, {{ source }}.to_tuple())) 27 | {% endmacro %} 28 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/xml.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class XML(BaseModel): 5 | """ 6 | A metadata object that allows for more fine-tuned XML model definitions. 7 | 8 | When using arrays, XML element names are *not* inferred (for singular/plural forms) 9 | and the `name` property SHOULD be used to add that information. 10 | See examples for expected behavior. 11 | 12 | References: 13 | - https://swagger.io/docs/specification/data-models/representing-xml/ 14 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject 15 | """ 16 | 17 | name: str | None = None 18 | namespace: str | None = None 19 | prefix: str | None = None 20 | attribute: bool = False 21 | wrapped: bool = False 22 | model_config = ConfigDict( 23 | extra="allow", 24 | json_schema_extra={ 25 | "examples": [ 26 | {"namespace": "http://example.com/schema/sample", "prefix": "sample"}, 27 | {"name": "aliens", "wrapped": True}, 28 | ] 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_date_property/test_date_property.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import jinja2 4 | 5 | from openapi_python_client.parser.properties import DateProperty 6 | 7 | 8 | def date_property(required=True, default=None) -> DateProperty: 9 | return DateProperty( 10 | name="a_prop", 11 | required=required, 12 | default=default, 13 | python_name="a_prop", 14 | description="", 15 | example="", 16 | ) 17 | 18 | 19 | def test_required(): 20 | prop = date_property() 21 | here = Path(__file__).parent 22 | templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates" 23 | 24 | env = jinja2.Environment( 25 | loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)]), 26 | trim_blocks=True, 27 | lstrip_blocks=True 28 | ) 29 | 30 | template = env.get_template("date_property_template.py.jinja") 31 | content = template.render(property=prop) 32 | expected = here / "required_not_null.py" 33 | assert content == expected.read_text() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Triax Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py: -------------------------------------------------------------------------------- 1 | from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture 2 | 3 | 4 | @with_generated_client_fixture( 5 | """ 6 | components: 7 | schemas: 8 | UnionWithInvalidReference: 9 | anyOf: 10 | - $ref: "#/components/schemas/DoesntExist" 11 | UnionWithInvalidDefault: 12 | type: ["number", "integer"] 13 | default: aaa 14 | UnionWithMalformedVariant: 15 | anyOf: 16 | - type: string 17 | - type: array # invalid because no items 18 | """ 19 | ) 20 | class TestUnionInvalidSchemas: 21 | def test_invalid_reference(self, generated_client): 22 | assert_bad_schema(generated_client, "UnionWithInvalidReference", "Could not find reference") 23 | 24 | def test_invalid_default(self, generated_client): 25 | assert_bad_schema(generated_client, "UnionWithInvalidDefault", "Invalid int value: aaa") 26 | 27 | def test_invalid_property(self, generated_client): 28 | assert_bad_schema(generated_client, "UnionWithMalformedVariant", "Invalid property in union") 29 | -------------------------------------------------------------------------------- /integration-tests/tests/test_api/test_parameters/test_post_parameters_header.py: -------------------------------------------------------------------------------- 1 | from integration_tests.api.parameters.post_parameters_header import sync_detailed 2 | from integration_tests.client import Client 3 | from integration_tests.models.post_parameters_header_response_200 import PostParametersHeaderResponse200 4 | 5 | 6 | def test(client: Client) -> None: 7 | string_header = "a test string" 8 | integer_header = 1 9 | number_header = 1.1 10 | boolean_header = True 11 | 12 | response = sync_detailed( 13 | client=client, 14 | boolean_header=boolean_header, 15 | string_header=string_header, 16 | integer_header=integer_header, 17 | number_header=number_header, 18 | ) 19 | 20 | parsed = response.parsed 21 | assert parsed is not None, f"{response.status_code}: {response.content!r}" 22 | assert isinstance( 23 | parsed, 24 | PostParametersHeaderResponse200, 25 | ), parsed 26 | assert parsed.string == string_header 27 | assert parsed.integer == integer_header 28 | assert parsed.number == number_header 29 | assert parsed.boolean == boolean_header 30 | -------------------------------------------------------------------------------- /tests/test_templates/test_property_templates/test_datetime_property/test_datetime_property.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import jinja2 4 | 5 | from openapi_python_client.parser.properties import DateTimeProperty 6 | 7 | 8 | def datetime_property(required=True, default=None) -> DateTimeProperty: 9 | return DateTimeProperty( 10 | name="a_prop", 11 | required=required, 12 | default=default, 13 | python_name="a_prop", 14 | description="", 15 | example="", 16 | ) 17 | 18 | 19 | def test_required(): 20 | prop = datetime_property() 21 | here = Path(__file__).parent 22 | templates_dir = here.parent.parent.parent.parent / "openapi_python_client" / "templates" 23 | 24 | env = jinja2.Environment( 25 | loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(here), jinja2.FileSystemLoader(templates_dir)]), 26 | trim_blocks=True, 27 | lstrip_blocks=True 28 | ) 29 | 30 | template = env.get_template("datetime_property_template.py.jinja") 31 | content = template.render(property=prop) 32 | expected = here / "required_not_null.py" 33 | assert content == expected.read_text() 34 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings_on_attributes.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: My Test API 4 | description: An API for testing docstrings_on_attributes behavior 5 | version: 0.1.0 6 | paths: 7 | {} 8 | components: 9 | schemas: 10 | ModelWithDescription: 11 | type: object 12 | description: This is a nice model. 13 | properties: 14 | propWithNoDesc: 15 | type: string 16 | propWithDesc: 17 | type: string 18 | description: This is a nice property. 19 | propWithLongDesc: 20 | type: string 21 | description: | 22 | It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, 23 | it was the epoch of belief, it was the epoch of incredulity, it was the season of light, it was the season of 24 | darkness, it was the spring of hope, it was the winter of despair. 25 | ModelWithNoDescription: 26 | type: object 27 | properties: 28 | propWithNoDesc: 29 | type: string 30 | propWithDesc: 31 | type: string 32 | description: This is a nice property. 33 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/example.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | 6 | class Example(BaseModel): 7 | """Examples added to parameters / components to help clarify usage. 8 | 9 | References: 10 | - https://swagger.io/docs/specification/adding-examples/ 11 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject 12 | """ 13 | 14 | summary: str | None = None 15 | description: str | None = None 16 | value: Any | None = None 17 | externalValue: str | None = None 18 | model_config = ConfigDict( 19 | extra="allow", 20 | json_schema_extra={ 21 | "examples": [ 22 | {"summary": "A foo example", "value": {"foo": "bar"}}, 23 | { 24 | "summary": "This is an example in XML", 25 | "externalValue": "http://example.org/examples/address-example.xml", 26 | }, 27 | {"summary": "This is a text example", "externalValue": "http://foo.bar/examples/address-example.txt"}, 28 | ] 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import ( 6 | delete_common_parameters_overriding_param, 7 | get_common_parameters_overriding_param, 8 | get_same_name_multiple_locations_param, 9 | multiple_path_parameters, 10 | ) 11 | 12 | 13 | class ParametersEndpoints: 14 | @classmethod 15 | def get_common_parameters_overriding_param(cls) -> types.ModuleType: 16 | """ 17 | Test that if you have an overriding property from `PathItem` in `Operation`, it produces valid code 18 | """ 19 | return get_common_parameters_overriding_param 20 | 21 | @classmethod 22 | def delete_common_parameters_overriding_param(cls) -> types.ModuleType: 23 | return delete_common_parameters_overriding_param 24 | 25 | @classmethod 26 | def get_same_name_multiple_locations_param(cls) -> types.ModuleType: 27 | return get_same_name_multiple_locations_param 28 | 29 | @classmethod 30 | def multiple_path_parameters(cls) -> types.ModuleType: 31 | return multiple_path_parameters 32 | -------------------------------------------------------------------------------- /openapi_python_client/parser/properties/property.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Property"] 2 | 3 | 4 | from typing import TypeAlias 5 | 6 | from .any import AnyProperty 7 | from .boolean import BooleanProperty 8 | from .const import ConstProperty 9 | from .date import DateProperty 10 | from .datetime import DateTimeProperty 11 | from .enum_property import EnumProperty 12 | from .file import FileProperty 13 | from .float import FloatProperty 14 | from .int import IntProperty 15 | from .list_property import ListProperty 16 | from .literal_enum_property import LiteralEnumProperty 17 | from .model_property import ModelProperty 18 | from .none import NoneProperty 19 | from .string import StringProperty 20 | from .union import UnionProperty 21 | from .uuid import UuidProperty 22 | 23 | Property: TypeAlias = ( 24 | AnyProperty 25 | | BooleanProperty 26 | | ConstProperty 27 | | DateProperty 28 | | DateTimeProperty 29 | | EnumProperty 30 | | LiteralEnumProperty 31 | | FileProperty 32 | | FloatProperty 33 | | IntProperty 34 | | ListProperty 35 | | ModelProperty 36 | | NoneProperty 37 | | StringProperty 38 | | UnionProperty 39 | | UuidProperty 40 | ) 41 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | 8 | from ..types import UNSET, Unset 9 | 10 | T = TypeVar("T", bound="TestInlineObjectsBody") 11 | 12 | 13 | @_attrs_define 14 | class TestInlineObjectsBody: 15 | """ 16 | Attributes: 17 | a_property (str | Unset): 18 | """ 19 | 20 | a_property: str | Unset = UNSET 21 | 22 | def to_dict(self) -> dict[str, Any]: 23 | a_property = self.a_property 24 | 25 | field_dict: dict[str, Any] = {} 26 | 27 | field_dict.update({}) 28 | if a_property is not UNSET: 29 | field_dict["a_property"] = a_property 30 | 31 | return field_dict 32 | 33 | @classmethod 34 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 35 | d = dict(src_dict) 36 | a_property = d.pop("a_property", UNSET) 37 | 38 | test_inline_objects_body = cls( 39 | a_property=a_property, 40 | ) 41 | 42 | return test_inline_objects_body 43 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kuimono 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | 8 | from ..types import UNSET, Unset 9 | 10 | T = TypeVar("T", bound="TestInlineObjectsResponse200") 11 | 12 | 13 | @_attrs_define 14 | class TestInlineObjectsResponse200: 15 | """ 16 | Attributes: 17 | a_property (str | Unset): 18 | """ 19 | 20 | a_property: str | Unset = UNSET 21 | 22 | def to_dict(self) -> dict[str, Any]: 23 | a_property = self.a_property 24 | 25 | field_dict: dict[str, Any] = {} 26 | 27 | field_dict.update({}) 28 | if a_property is not UNSET: 29 | field_dict["a_property"] = a_property 30 | 31 | return field_dict 32 | 33 | @classmethod 34 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 35 | d = dict(src_dict) 36 | a_property = d.pop("a_property", UNSET) 37 | 38 | test_inline_objects_response_200 = cls( 39 | a_property=a_property, 40 | ) 41 | 42 | return test_inline_objects_response_200 43 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import ( 6 | get_common_parameters, 7 | get_models_allof, 8 | get_models_oneof_with_required_const, 9 | post_common_parameters, 10 | post_types_unions_duplicate_types, 11 | reserved_parameters, 12 | ) 13 | 14 | 15 | class DefaultEndpoints: 16 | @classmethod 17 | def get_common_parameters(cls) -> types.ModuleType: 18 | return get_common_parameters 19 | 20 | @classmethod 21 | def post_common_parameters(cls) -> types.ModuleType: 22 | return post_common_parameters 23 | 24 | @classmethod 25 | def reserved_parameters(cls) -> types.ModuleType: 26 | return reserved_parameters 27 | 28 | @classmethod 29 | def get_models_allof(cls) -> types.ModuleType: 30 | return get_models_allof 31 | 32 | @classmethod 33 | def get_models_oneof_with_required_const(cls) -> types.ModuleType: 34 | return get_models_oneof_with_required_const 35 | 36 | @classmethod 37 | def post_types_unions_duplicate_types(cls) -> types.ModuleType: 38 | return post_types_unions_duplicate_types 39 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/uuid_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | UUID({{ source }}) 3 | {% endmacro %} 4 | 5 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 6 | 7 | {% macro construct(property, source) %} 8 | {{ construct_template(construct_function, property, source) }} 9 | {% endmacro %} 10 | 11 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} 12 | 13 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 14 | {% set transformed = "str(" + source + ")" %} 15 | {% if property.required %} 16 | {{ destination }} = {{ transformed }} 17 | {%- else %} 18 | {% if not skip_unset %} 19 | {% if declare_type %} 20 | {% set type_annotation = property.get_type_string(json=True) %} 21 | {{ destination }}: {{ type_annotation }} = UNSET 22 | {% else %} 23 | {{ destination }} = UNSET 24 | {% endif %} 25 | {% endif %} 26 | if not isinstance({{ source }}, Unset): 27 | {{ destination }} = {{ transformed }} 28 | {%- endif %} 29 | {% endmacro %} 30 | 31 | {% macro multipart(property, source, name) %} 32 | files.append(({{ name }}, (None, str({{ source }}), "text/plain"))) 33 | {% endmacro %} 34 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/date_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | isoparse({{ source }}).date() 3 | {% endmacro %} 4 | 5 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 6 | 7 | {% macro construct(property, source) %} 8 | {{ construct_template(construct_function, property, source) }} 9 | {% endmacro %} 10 | 11 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} 12 | 13 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 14 | {% set transformed = source + ".isoformat()" %} 15 | {% if property.required %} 16 | {{ destination }} = {{ transformed }} 17 | {%- else %} 18 | {% if not skip_unset %} 19 | {% if declare_type %} 20 | {% set type_annotation = property.get_type_string(json=True) %} 21 | {{ destination }}: {{ type_annotation }} = UNSET 22 | {% else %} 23 | {{ destination }} = UNSET 24 | {% endif %} 25 | {% endif %} 26 | if not isinstance({{ source }}, Unset): 27 | {{ destination }} = {{ transformed }} 28 | {%- endif %} 29 | {% endmacro %} 30 | 31 | {% macro multipart(property, source, name) %} 32 | files.append(({{ name }}, (None, {{ source }}.isoformat().encode(), "text/plain"))) 33 | {% endmacro %} 34 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class OAuthFlow(BaseModel): 5 | """ 6 | Configuration details for a supported OAuth Flow 7 | 8 | References: 9 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject 10 | - https://swagger.io/docs/specification/authentication/oauth2/ 11 | """ 12 | 13 | authorizationUrl: str | None = None 14 | tokenUrl: str | None = None 15 | refreshUrl: str | None = None 16 | scopes: dict[str, str] 17 | model_config = ConfigDict( 18 | extra="allow", 19 | json_schema_extra={ 20 | "examples": [ 21 | { 22 | "authorizationUrl": "https://example.com/api/oauth/dialog", 23 | "scopes": {"write:pets": "modify pets in your account", "read:pets": "read your pets"}, 24 | }, 25 | { 26 | "authorizationUrl": "https://example.com/api/oauth/dialog", 27 | "tokenUrl": "https://example.com/api/oauth/token", 28 | "scopes": {"write:pets": "modify pets in your account", "read:pets": "read your pets"}, 29 | }, 30 | ] 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/datetime_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | isoparse({{ source }}) 3 | {% endmacro %} 4 | 5 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 6 | 7 | {% macro construct(property, source) %} 8 | {{ construct_template(construct_function, property, source) }} 9 | {% endmacro %} 10 | 11 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} 12 | 13 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 14 | {% set transformed = source + ".isoformat()" %} 15 | {% if property.required %} 16 | {{ destination }} = {{ transformed }} 17 | {%- else %} 18 | {% if not skip_unset %} 19 | {% if declare_type %} 20 | {% set type_annotation = property.get_type_string(json=True) %} 21 | {{ destination }}: {{ type_annotation }} = UNSET 22 | {% else %} 23 | {{ destination }} = UNSET 24 | {% endif %} 25 | {% endif %} 26 | if not isinstance({{ source }}, Unset): 27 | {{ destination }} = {{ transformed }} 28 | {%- endif %} 29 | {% endmacro %} 30 | 31 | {% macro multipart(property, source, name) %} 32 | files.append(({{ name }}, (None, {{ source }}.isoformat().encode(), "text/plain"))) 33 | {% endmacro %} 34 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/enum_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | {{ property.class_info.name }}({{ source }}) 3 | {% endmacro %} 4 | 5 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 6 | 7 | {% macro construct(property, source) %} 8 | {{ construct_template(construct_function, property, source) }} 9 | {% endmacro %} 10 | 11 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %} 12 | 13 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 14 | {% set transformed = source + ".value" %} 15 | {% set type_string = property.get_type_string(json=True) %} 16 | {% if property.required %} 17 | {{ destination }} = {{ transformed }} 18 | {%- else %} 19 | {% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif +%} 20 | if not isinstance({{ source }}, Unset): 21 | {{ destination }} = {{ transformed }} 22 | {% endif %} 23 | {% endmacro %} 24 | 25 | {% macro multipart(property, source, name) %} 26 | files.append(({{ name }}, (None, str({{ source }}.value).encode(), "text/plain"))) 27 | {% endmacro %} 28 | 29 | {% macro transform_header(source) %} 30 | str({{ source }}) 31 | {% endmacro %} 32 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/literal_enum_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | check_{{ property.get_class_name_snake_case() }}({{ source }}) 3 | {% endmacro %} 4 | 5 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 6 | 7 | {% macro construct(property, source) %} 8 | {{ construct_template(construct_function, property, source) }} 9 | {% endmacro %} 10 | 11 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.get_instance_type_string() }}){% endmacro %} 12 | 13 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 14 | {% set type_string = property.get_type_string(json=True) %} 15 | {% if property.required %} 16 | {{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = {{ source }} 17 | {%- else %} 18 | {% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif +%} 19 | if not isinstance({{ source }}, Unset): 20 | {{ destination }} = {{ source }} 21 | {% endif %} 22 | {% endmacro %} 23 | 24 | {% macro multipart(property, source, name) %} 25 | files.append(({{ name }}, (None, str({{ source }}).encode(), "text/plain"))) 26 | {% endmacro %} 27 | 28 | {% macro transform_header(source) %} 29 | str({{ source }}) 30 | {% endmacro %} 31 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar, cast 5 | 6 | from attrs import define as _attrs_define 7 | 8 | T = TypeVar("T", bound="ValidationError") 9 | 10 | 11 | @_attrs_define 12 | class ValidationError: 13 | """ 14 | Attributes: 15 | loc (list[str]): 16 | msg (str): 17 | type_ (str): 18 | """ 19 | 20 | loc: list[str] 21 | msg: str 22 | type_: str 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | loc = self.loc 26 | 27 | msg = self.msg 28 | 29 | type_ = self.type_ 30 | 31 | field_dict: dict[str, Any] = {} 32 | 33 | field_dict.update( 34 | { 35 | "loc": loc, 36 | "msg": msg, 37 | "type": type_, 38 | } 39 | ) 40 | 41 | return field_dict 42 | 43 | @classmethod 44 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 45 | d = dict(src_dict) 46 | loc = cast(list[str], d.pop("loc")) 47 | 48 | msg = d.pop("msg") 49 | 50 | type_ = d.pop("type") 51 | 52 | validation_error = cls( 53 | loc=loc, 54 | msg=msg, 55 | type_=type_, 56 | ) 57 | 58 | return validation_error 59 | -------------------------------------------------------------------------------- /openapi_python_client/parser/errors.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | __all__ = ["ErrorLevel", "GeneratorError", "ParameterError", "ParseError", "PropertyError"] 5 | 6 | from pydantic import BaseModel 7 | 8 | 9 | class ErrorLevel(Enum): 10 | """The level of an error""" 11 | 12 | WARNING = "WARNING" # Client is still generated but missing some pieces 13 | ERROR = "ERROR" # Client could not be generated 14 | 15 | 16 | @dataclass 17 | class GeneratorError: 18 | """Base data struct containing info on an error that occurred""" 19 | 20 | detail: str | None = None 21 | level: ErrorLevel = ErrorLevel.ERROR 22 | header: str = "Unable to generate the client" 23 | 24 | 25 | @dataclass 26 | class ParseError(GeneratorError): 27 | """An error raised when there's a problem parsing an OpenAPI document""" 28 | 29 | level: ErrorLevel = ErrorLevel.WARNING 30 | data: BaseModel | None = None 31 | header: str = "Unable to parse this part of your OpenAPI document: " 32 | 33 | 34 | @dataclass 35 | class PropertyError(ParseError): 36 | """Error raised when there's a problem creating a Property""" 37 | 38 | header = "Problem creating a Property: " 39 | 40 | 41 | @dataclass 42 | class ParameterError(ParseError): 43 | """Error raised when there's a problem creating a Parameter.""" 44 | 45 | header = "Problem creating a Parameter: " 46 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/discriminator.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | 4 | class Discriminator(BaseModel): 5 | """ 6 | When request bodies or response payloads may be one of a number of different schemas, 7 | a `discriminator` object can be used to aid in serialization, deserialization, and validation. 8 | 9 | The discriminator is a specific object in a schema which is used to inform the consumer of the specification 10 | of an alternative schema based on the value associated with it. 11 | 12 | When using the discriminator, _inline_ schemas will not be considered. 13 | 14 | References: 15 | - https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/ 16 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject 17 | """ 18 | 19 | propertyName: str 20 | mapping: dict[str, str] | None = None 21 | model_config = ConfigDict( 22 | extra="allow", 23 | json_schema_extra={ 24 | "examples": [ 25 | { 26 | "propertyName": "petType", 27 | "mapping": { 28 | "dog": "#/components/schemas/Dog", 29 | "monster": "https://gigantic-server.com/schemas/Monster/schema.json", 30 | }, 31 | } 32 | ] 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/none.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="None_") 10 | 11 | 12 | @_attrs_define 13 | class None_: 14 | """ """ 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | none = cls() 28 | 29 | none.additional_properties = d 30 | return none 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/import_.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="Import") 10 | 11 | 12 | @_attrs_define 13 | class Import: 14 | """ """ 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | import_ = cls() 28 | 29 | import_.additional_properties = d 30 | return import_ 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/header.py: -------------------------------------------------------------------------------- 1 | from pydantic import ConfigDict, Field 2 | 3 | from ..parameter_location import ParameterLocation 4 | from .parameter import Parameter 5 | 6 | 7 | class Header(Parameter): 8 | """ 9 | The Header Object follows the structure of the [Parameter Object](#parameterObject) with the following changes: 10 | 11 | 1. `name` MUST NOT be specified, it is given in the corresponding `headers` map. 12 | 2. `in` MUST NOT be specified, it is implicitly in `header`. 13 | 3. All traits that are affected by the location MUST be applicable to a location of `header` 14 | (for example, [`style`](#parameterStyle)). 15 | 16 | References: 17 | - https://swagger.io/docs/specification/describing-parameters/#header-parameters 18 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject 19 | """ 20 | 21 | name: str = Field(default="") 22 | param_in: ParameterLocation = Field(default=ParameterLocation.HEADER, alias="in") 23 | model_config = ConfigDict( 24 | # `Parameter` is not build yet, will rebuild in `__init__.py`: 25 | defer_build=True, 26 | extra="allow", 27 | populate_by_name=True, 28 | json_schema_extra={ 29 | "examples": [ 30 | {"description": "The number of allowed requests in the current period", "schema": {"type": "integer"}} 31 | ] 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_name.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="ModelName") 10 | 11 | 12 | @_attrs_define 13 | class ModelName: 14 | """ """ 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | model_name = cls() 28 | 29 | model_name.additional_properties = d 30 | return model_name 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | 3 | from openapi_python_client.cli import app 4 | 5 | runner = CliRunner() 6 | 7 | 8 | def test_version() -> None: 9 | result = runner.invoke(app, ["--version", "generate"]) 10 | 11 | assert result.exit_code == 0 12 | assert "openapi-python-client version: " in result.stdout 13 | 14 | 15 | def test_bad_config() -> None: 16 | config_path = "config/path" 17 | path = "cool/path" 18 | 19 | result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={path}"]) 20 | 21 | assert result.exit_code == 2 22 | assert "Unable to parse config" in result.output 23 | 24 | 25 | class TestGenerate: 26 | def test_generate_no_params(self) -> None: 27 | result = runner.invoke(app, ["generate"]) 28 | 29 | assert result.exit_code == 1, result.output 30 | 31 | def test_generate_url_and_path(self) -> None: 32 | result = runner.invoke(app, ["generate", "--path=blah", "--url=otherblah"]) 33 | 34 | assert result.exit_code == 1 35 | assert result.output == "Provide either --url or --path, not both\n" 36 | 37 | def test_generate_encoding_errors(self) -> None: 38 | path = "cool/path" 39 | file_encoding = "error-file-encoding" 40 | result = runner.invoke(app, ["generate", f"--path={path}", f"--file-encoding={file_encoding}"]) 41 | 42 | assert result.exit_code == 1 43 | assert result.output == f"Unknown encoding : {file_encoding}\n" 44 | -------------------------------------------------------------------------------- /tests/test_schema/test_open_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError 3 | 4 | from openapi_python_client.schema import OpenAPI 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "version, valid", 9 | [ 10 | ("abc", False), 11 | ("1", False), 12 | ("2.0", False), 13 | ("3.0.0", True), 14 | ("3.1.1", True), 15 | ("3.2.0", False), 16 | ("4.0.0", False), 17 | ], 18 | ) 19 | def test_validate_version(version, valid): 20 | data = {"openapi": version, "info": {"title": "test", "version": ""}, "paths": {}} 21 | if valid: 22 | OpenAPI.model_validate(data) 23 | else: 24 | with pytest.raises(ValidationError): 25 | OpenAPI.model_validate(data) 26 | 27 | 28 | def test_parse_with_callback(): 29 | data = { 30 | "openapi": "3.0.1", 31 | "info": {"title": "API with Callback", "version": ""}, 32 | "paths": { 33 | "/create": { 34 | "post": { 35 | "responses": {"200": {"description": "Success"}}, 36 | "callbacks": {"event": {"callback": {"post": {"responses": {"200": {"description": "Success"}}}}}}, 37 | } 38 | } 39 | }, 40 | } 41 | 42 | open_api = OpenAPI.model_validate(data) 43 | create_endpoint = open_api.paths["/create"] 44 | assert "200" in create_endpoint.post.responses 45 | assert "200" in create_endpoint.post.callbacks["event"]["callback"].post.responses 46 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="FreeFormModel") 10 | 11 | 12 | @_attrs_define 13 | class FreeFormModel: 14 | """ """ 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | free_form_model = cls() 28 | 29 | free_form_model.additional_properties = d 30 | return free_form_model 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/optional_body_body.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="OptionalBodyBody") 10 | 11 | 12 | @_attrs_define 13 | class OptionalBodyBody: 14 | """ """ 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | optional_body_body = cls() 28 | 29 | optional_body_body.additional_properties = d 30 | return optional_body_body 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/encoding.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | from .reference import ReferenceOr 6 | 7 | if TYPE_CHECKING: # pragma: no cover 8 | from .header import Header 9 | 10 | 11 | class Encoding(BaseModel): 12 | """A single encoding definition applied to a single schema property. 13 | 14 | References: 15 | - https://swagger.io/docs/specification/describing-request-body/ 16 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject 17 | """ 18 | 19 | contentType: str | None = None 20 | headers: dict[str, ReferenceOr["Header"]] | None = None 21 | style: str | None = None 22 | explode: bool = False 23 | allowReserved: bool = False 24 | model_config = ConfigDict( 25 | # `Header` is an unresolvable forward reference, will rebuild in `__init__.py`: 26 | defer_build=True, 27 | extra="allow", 28 | json_schema_extra={ 29 | "examples": [ 30 | { 31 | "contentType": "image/png, image/jpeg", 32 | "headers": { 33 | "X-Rate-Limit-Limit": { 34 | "description": "The number of allowed requests in the current period", 35 | "schema": {"type": "integer"}, 36 | } 37 | }, 38 | } 39 | ] 40 | }, 41 | ) 42 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/server.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | from .server_variable import ServerVariable 4 | 5 | 6 | class Server(BaseModel): 7 | """An object representing a Server. 8 | 9 | References: 10 | - https://swagger.io/docs/specification/api-host-and-base-path/ 11 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject 12 | """ 13 | 14 | url: str 15 | description: str | None = None 16 | variables: dict[str, ServerVariable] | None = None 17 | model_config = ConfigDict( 18 | extra="allow", 19 | json_schema_extra={ 20 | "examples": [ 21 | {"url": "https://development.gigantic-server.com/v1", "description": "Development server"}, 22 | { 23 | "url": "https://{username}.gigantic-server.com:{port}/{basePath}", 24 | "description": "The production API server", 25 | "variables": { 26 | "username": { 27 | "default": "demo", 28 | "description": "this value is assigned by the service provider, " 29 | "in this example `gigantic-server.com`", 30 | }, 31 | "port": {"enum": ["8443", "443"], "default": "8443"}, 32 | "basePath": {"default": "v2"}, 33 | }, 34 | }, 35 | ] 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /openapi_python_client/parser/properties/any.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, ClassVar 4 | 5 | from attr import define 6 | 7 | from ...utils import PythonIdentifier 8 | from .protocol import PropertyProtocol, Value 9 | 10 | 11 | @define 12 | class AnyProperty(PropertyProtocol): 13 | """A property that can be any type (used for empty schemas)""" 14 | 15 | @classmethod 16 | def build( 17 | cls, 18 | name: str, 19 | required: bool, 20 | default: Any, 21 | python_name: PythonIdentifier, 22 | description: str | None, 23 | example: str | None, 24 | ) -> AnyProperty: 25 | return cls( 26 | name=name, 27 | required=required, 28 | default=AnyProperty.convert_value(default), 29 | python_name=python_name, 30 | description=description, 31 | example=example, 32 | ) 33 | 34 | @classmethod 35 | def convert_value(cls, value: Any) -> Value | None: 36 | from .string import StringProperty # noqa: PLC0415 37 | 38 | if value is None: 39 | return value 40 | if isinstance(value, str): 41 | return StringProperty.convert_value(value) 42 | return Value(python_code=str(value), raw_value=value) 43 | 44 | name: str 45 | required: bool 46 | default: Value | None 47 | python_name: PythonIdentifier 48 | description: str | None 49 | example: str | None 50 | _type_string: ClassVar[str] = "Any" 51 | _json_type_string: ClassVar[str] = "Any" 52 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/types.py: -------------------------------------------------------------------------------- 1 | """Contains some shared types for properties""" 2 | 3 | from collections.abc import Mapping, MutableMapping 4 | from http import HTTPStatus 5 | from typing import IO, BinaryIO, Generic, Literal, TypeVar 6 | 7 | from attrs import define 8 | 9 | 10 | class Unset: 11 | def __bool__(self) -> Literal[False]: 12 | return False 13 | 14 | 15 | UNSET: Unset = Unset() 16 | 17 | # The types that `httpx.Client(files=)` can accept, copied from that library. 18 | FileContent = IO[bytes] | bytes | str 19 | FileTypes = ( 20 | # (filename, file (or bytes), content_type) 21 | tuple[str | None, FileContent, str | None] 22 | # (filename, file (or bytes), content_type, headers) 23 | | tuple[str | None, FileContent, str | None, Mapping[str, str]] 24 | ) 25 | RequestFiles = list[tuple[str, FileTypes]] 26 | 27 | 28 | @define 29 | class File: 30 | """Contains information for file uploads""" 31 | 32 | payload: BinaryIO 33 | file_name: str | None = None 34 | mime_type: str | None = None 35 | 36 | def to_tuple(self) -> FileTypes: 37 | """Return a tuple representation that httpx will accept for multipart/form-data""" 38 | return self.file_name, self.payload, self.mime_type 39 | 40 | 41 | T = TypeVar("T") 42 | 43 | 44 | @define 45 | class Response(Generic[T]): 46 | """A response from an endpoint""" 47 | 48 | status_code: HTTPStatus 49 | content: bytes 50 | headers: MutableMapping[str, str] 51 | parsed: T | None 52 | 53 | 54 | __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] 55 | -------------------------------------------------------------------------------- /openapi_python_client/templates/property_templates/model_property.py.jinja: -------------------------------------------------------------------------------- 1 | {% macro construct_function(property, source) %} 2 | {{ property.class_info.name }}.from_dict({{ source }}) 3 | {% endmacro %} 4 | 5 | {% from "property_templates/property_macros.py.jinja" import construct_template %} 6 | 7 | {% macro construct(property, source) %} 8 | {{ construct_template(construct_function, property, source) }} 9 | {% endmacro %} 10 | 11 | {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, dict){% endmacro %} 12 | 13 | {% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} 14 | {% set transformed = source + ".to_dict()" %} 15 | {% set type_string = property.get_type_string(json=True) %} 16 | {% if property.required %} 17 | {{ destination }} = {{ transformed }} 18 | {%- else %} 19 | {% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif %} 20 | 21 | if not isinstance({{ source }}, Unset): 22 | {{ destination }} = {{ transformed }} 23 | {%- endif %} 24 | {% endmacro %} 25 | 26 | {% macro transform_multipart_body(property) %} 27 | {% set transformed = property.python_name + ".to_multipart()" %} 28 | {% if property.required %} 29 | _kwargs["files"] = {{ transformed }} 30 | {%- else %} 31 | if not isinstance({{ property.python_name }}, Unset): 32 | _kwargs["files"] = {{ transformed }} 33 | {%- endif %} 34 | {% endmacro %} 35 | 36 | {% macro multipart(property, source, name) %} 37 | files.append(({{ name }}, (None, json.dumps( {{source}}.to_dict()).encode(), "application/json"))) 38 | {% endmacro %} 39 | -------------------------------------------------------------------------------- /openapi_python_client/templates/types.py.jinja: -------------------------------------------------------------------------------- 1 | """ Contains some shared types for properties """ 2 | 3 | from collections.abc import Mapping, MutableMapping 4 | from http import HTTPStatus 5 | from typing import BinaryIO, Generic, TypeVar, Literal, IO 6 | 7 | from attrs import define 8 | 9 | 10 | class Unset: 11 | def __bool__(self) -> Literal[False]: 12 | return False 13 | 14 | 15 | UNSET: Unset = Unset() 16 | 17 | # The types that `httpx.Client(files=)` can accept, copied from that library. 18 | FileContent = IO[bytes] | bytes | str 19 | FileTypes = ( 20 | # (filename, file (or bytes), content_type) 21 | tuple[str | None, FileContent, str | None] 22 | # (filename, file (or bytes), content_type, headers) 23 | | tuple[str | None, FileContent, str | None, Mapping[str, str]] 24 | ) 25 | RequestFiles = list[tuple[str, FileTypes]] 26 | 27 | @define 28 | class File: 29 | """ Contains information for file uploads """ 30 | 31 | payload: BinaryIO 32 | file_name: str | None = None 33 | mime_type: str | None = None 34 | 35 | def to_tuple(self) -> FileTypes: 36 | """ Return a tuple representation that httpx will accept for multipart/form-data """ 37 | return self.file_name, self.payload, self.mime_type 38 | 39 | 40 | T = TypeVar("T") 41 | 42 | 43 | @define 44 | class Response(Generic[T]): 45 | """ A response from an endpoint """ 46 | 47 | status_code: HTTPStatus 48 | content: bytes 49 | headers: MutableMapping[str, str] 50 | parsed: T | None 51 | 52 | 53 | __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] 54 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_reference_with_periods.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="ModelReferenceWithPeriods") 10 | 11 | 12 | @_attrs_define 13 | class ModelReferenceWithPeriods: 14 | """A Model with periods in its reference""" 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | model_reference_with_periods = cls() 28 | 29 | model_reference_with_periods.additional_properties = d 30 | return model_reference_with_periods 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/types.py: -------------------------------------------------------------------------------- 1 | """Contains some shared types for properties""" 2 | 3 | from collections.abc import Mapping, MutableMapping 4 | from http import HTTPStatus 5 | from typing import IO, BinaryIO, Generic, Literal, TypeVar 6 | 7 | from attrs import define 8 | 9 | 10 | class Unset: 11 | def __bool__(self) -> Literal[False]: 12 | return False 13 | 14 | 15 | UNSET: Unset = Unset() 16 | 17 | # The types that `httpx.Client(files=)` can accept, copied from that library. 18 | FileContent = IO[bytes] | bytes | str 19 | FileTypes = ( 20 | # (filename, file (or bytes), content_type) 21 | tuple[str | None, FileContent, str | None] 22 | # (filename, file (or bytes), content_type, headers) 23 | | tuple[str | None, FileContent, str | None, Mapping[str, str]] 24 | ) 25 | RequestFiles = list[tuple[str, FileTypes]] 26 | 27 | 28 | @define 29 | class File: 30 | """Contains information for file uploads""" 31 | 32 | payload: BinaryIO 33 | file_name: str | None = None 34 | mime_type: str | None = None 35 | 36 | def to_tuple(self) -> FileTypes: 37 | """Return a tuple representation that httpx will accept for multipart/form-data""" 38 | return self.file_name, self.payload, self.mime_type 39 | 40 | 41 | T = TypeVar("T") 42 | 43 | 44 | @define 45 | class Response(Generic[T]): 46 | """A response from an endpoint""" 47 | 48 | status_code: HTTPStatus 49 | content: bytes 50 | headers: MutableMapping[str, str] 51 | parsed: T | None 52 | 53 | 54 | __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] 55 | -------------------------------------------------------------------------------- /end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py: -------------------------------------------------------------------------------- 1 | """Contains some shared types for properties""" 2 | 3 | from collections.abc import Mapping, MutableMapping 4 | from http import HTTPStatus 5 | from typing import IO, BinaryIO, Generic, Literal, TypeVar 6 | 7 | from attrs import define 8 | 9 | 10 | class Unset: 11 | def __bool__(self) -> Literal[False]: 12 | return False 13 | 14 | 15 | UNSET: Unset = Unset() 16 | 17 | # The types that `httpx.Client(files=)` can accept, copied from that library. 18 | FileContent = IO[bytes] | bytes | str 19 | FileTypes = ( 20 | # (filename, file (or bytes), content_type) 21 | tuple[str | None, FileContent, str | None] 22 | # (filename, file (or bytes), content_type, headers) 23 | | tuple[str | None, FileContent, str | None, Mapping[str, str]] 24 | ) 25 | RequestFiles = list[tuple[str, FileTypes]] 26 | 27 | 28 | @define 29 | class File: 30 | """Contains information for file uploads""" 31 | 32 | payload: BinaryIO 33 | file_name: str | None = None 34 | mime_type: str | None = None 35 | 36 | def to_tuple(self) -> FileTypes: 37 | """Return a tuple representation that httpx will accept for multipart/form-data""" 38 | return self.file_name, self.payload, self.mime_type 39 | 40 | 41 | T = TypeVar("T") 42 | 43 | 44 | @define 45 | class Response(Generic[T]): 46 | """A response from an endpoint""" 47 | 48 | status_code: HTTPStatus 49 | content: bytes 50 | headers: MutableMapping[str, str] 51 | parsed: T | None 52 | 53 | 54 | __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] 55 | -------------------------------------------------------------------------------- /end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py: -------------------------------------------------------------------------------- 1 | """Contains some shared types for properties""" 2 | 3 | from collections.abc import Mapping, MutableMapping 4 | from http import HTTPStatus 5 | from typing import IO, BinaryIO, Generic, Literal, TypeVar 6 | 7 | from attrs import define 8 | 9 | 10 | class Unset: 11 | def __bool__(self) -> Literal[False]: 12 | return False 13 | 14 | 15 | UNSET: Unset = Unset() 16 | 17 | # The types that `httpx.Client(files=)` can accept, copied from that library. 18 | FileContent = IO[bytes] | bytes | str 19 | FileTypes = ( 20 | # (filename, file (or bytes), content_type) 21 | tuple[str | None, FileContent, str | None] 22 | # (filename, file (or bytes), content_type, headers) 23 | | tuple[str | None, FileContent, str | None, Mapping[str, str]] 24 | ) 25 | RequestFiles = list[tuple[str, FileTypes]] 26 | 27 | 28 | @define 29 | class File: 30 | """Contains information for file uploads""" 31 | 32 | payload: BinaryIO 33 | file_name: str | None = None 34 | mime_type: str | None = None 35 | 36 | def to_tuple(self) -> FileTypes: 37 | """Return a tuple representation that httpx will accept for multipart/form-data""" 38 | return self.file_name, self.payload, self.mime_type 39 | 40 | 41 | T = TypeVar("T") 42 | 43 | 44 | @define 45 | class Response(Generic[T]): 46 | """A response from an endpoint""" 47 | 48 | status_code: HTTPStatus 49 | content: bytes 50 | headers: MutableMapping[str, str] 51 | parsed: T | None 52 | 53 | 54 | __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] 55 | -------------------------------------------------------------------------------- /end_to_end_tests/docstrings-on-attributes-golden-record/my_test_api_client/types.py: -------------------------------------------------------------------------------- 1 | """Contains some shared types for properties""" 2 | 3 | from collections.abc import Mapping, MutableMapping 4 | from http import HTTPStatus 5 | from typing import IO, BinaryIO, Generic, Literal, TypeVar 6 | 7 | from attrs import define 8 | 9 | 10 | class Unset: 11 | def __bool__(self) -> Literal[False]: 12 | return False 13 | 14 | 15 | UNSET: Unset = Unset() 16 | 17 | # The types that `httpx.Client(files=)` can accept, copied from that library. 18 | FileContent = IO[bytes] | bytes | str 19 | FileTypes = ( 20 | # (filename, file (or bytes), content_type) 21 | tuple[str | None, FileContent, str | None] 22 | # (filename, file (or bytes), content_type, headers) 23 | | tuple[str | None, FileContent, str | None, Mapping[str, str]] 24 | ) 25 | RequestFiles = list[tuple[str, FileTypes]] 26 | 27 | 28 | @define 29 | class File: 30 | """Contains information for file uploads""" 31 | 32 | payload: BinaryIO 33 | file_name: str | None = None 34 | mime_type: str | None = None 35 | 36 | def to_tuple(self) -> FileTypes: 37 | """Return a tuple representation that httpx will accept for multipart/form-data""" 38 | return self.file_name, self.payload, self.mime_type 39 | 40 | 41 | T = TypeVar("T") 42 | 43 | 44 | @define 45 | class Response(Generic[T]): 46 | """A response from an endpoint""" 47 | 48 | status_code: HTTPStatus 49 | content: bytes 50 | headers: MutableMapping[str, str] 51 | parsed: T | None 52 | 53 | 54 | __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] 55 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/info.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | 3 | from .contact import Contact 4 | from .license import License 5 | 6 | 7 | class Info(BaseModel): 8 | """ 9 | The object provides metadata about the API. 10 | The metadata MAY be used by the clients if needed, 11 | and MAY be presented in editing or documentation generation tools for convenience. 12 | 13 | References: 14 | - https://swagger.io/docs/specification/api-general-info/ 15 | -https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject 16 | """ 17 | 18 | title: str 19 | description: str | None = None 20 | termsOfService: str | None = None 21 | contact: Contact | None = None 22 | license: License | None = None 23 | version: str 24 | model_config = ConfigDict( 25 | extra="allow", 26 | json_schema_extra={ 27 | "examples": [ 28 | { 29 | "title": "Sample Pet Store App", 30 | "description": "This is a sample server for a pet store.", 31 | "termsOfService": "http://example.com/terms/", 32 | "contact": { 33 | "name": "API Support", 34 | "url": "http://www.example.com/support", 35 | "email": "support@example.com", 36 | }, 37 | "license": {"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"}, 38 | "version": "1.0.1", 39 | } 40 | ] 41 | }, 42 | ) 43 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="ModelWithBackslashInDescription") 10 | 11 | 12 | @_attrs_define 13 | class ModelWithBackslashInDescription: 14 | r""" Description with special character: \ 15 | 16 | """ 17 | 18 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 19 | 20 | def to_dict(self) -> dict[str, Any]: 21 | field_dict: dict[str, Any] = {} 22 | field_dict.update(self.additional_properties) 23 | 24 | return field_dict 25 | 26 | @classmethod 27 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 28 | d = dict(src_dict) 29 | model_with_backslash_in_description = cls() 30 | 31 | model_with_backslash_in_description.additional_properties = d 32 | return model_with_backslash_in_description 33 | 34 | @property 35 | def additional_keys(self) -> list[str]: 36 | return list(self.additional_properties.keys()) 37 | 38 | def __getitem__(self, key: str) -> Any: 39 | return self.additional_properties[key] 40 | 41 | def __setitem__(self, key: str, value: Any) -> None: 42 | self.additional_properties[key] = value 43 | 44 | def __delitem__(self, key: str) -> None: 45 | del self.additional_properties[key] 46 | 47 | def __contains__(self, key: str) -> bool: 48 | return key in self.additional_properties 49 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/reference.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Any, Literal, TypeAlias, TypeVar 2 | 3 | from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag 4 | 5 | 6 | class Reference(BaseModel): 7 | """ 8 | A simple object to allow referencing other components in the specification, internally and externally. 9 | 10 | The Reference Object is defined by [JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03) 11 | and follows the same structure, behavior and rules. 12 | 13 | For this specification, reference resolution is accomplished as defined by the JSON Reference specification 14 | and not by the JSON Schema specification. 15 | 16 | References: 17 | - https://swagger.io/docs/specification/using-ref/ 18 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject 19 | """ 20 | 21 | ref: str = Field(alias="$ref") 22 | model_config = ConfigDict( 23 | extra="allow", 24 | populate_by_name=True, 25 | json_schema_extra={ 26 | "examples": [{"$ref": "#/components/schemas/Pet"}, {"$ref": "Pet.json"}, {"$ref": "definitions.json#/Pet"}] 27 | }, 28 | ) 29 | 30 | 31 | T = TypeVar("T") 32 | 33 | 34 | def _reference_discriminator(obj: Any) -> Literal["ref", "other"]: 35 | if isinstance(obj, dict): 36 | return "ref" if "$ref" in obj else "other" 37 | return "ref" if isinstance(obj, Reference) else "other" 38 | 39 | 40 | ReferenceOr: TypeAlias = Annotated[ 41 | Annotated[Reference, Tag("ref")] | Annotated[T, Tag("other")], Discriminator(_reference_discriminator) 42 | ] 43 | -------------------------------------------------------------------------------- /end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/responses/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains methods for accessing the API Endpoints""" 2 | 3 | import types 4 | 5 | from . import ( 6 | default_status_code, 7 | post_responses_unions_simple_before_complex, 8 | reference_response, 9 | status_code_patterns, 10 | status_code_precedence, 11 | text_response, 12 | ) 13 | 14 | 15 | class ResponsesEndpoints: 16 | @classmethod 17 | def post_responses_unions_simple_before_complex(cls) -> types.ModuleType: 18 | """ 19 | Regression test for #603 20 | """ 21 | return post_responses_unions_simple_before_complex 22 | 23 | @classmethod 24 | def text_response(cls) -> types.ModuleType: 25 | """ 26 | Text Response 27 | """ 28 | return text_response 29 | 30 | @classmethod 31 | def reference_response(cls) -> types.ModuleType: 32 | """ 33 | Endpoint using predefined response 34 | """ 35 | return reference_response 36 | 37 | @classmethod 38 | def default_status_code(cls) -> types.ModuleType: 39 | """ 40 | Default Status Code Only 41 | """ 42 | return default_status_code 43 | 44 | @classmethod 45 | def status_code_patterns(cls) -> types.ModuleType: 46 | """ 47 | Status Code Patterns 48 | """ 49 | return status_code_patterns 50 | 51 | @classmethod 52 | def status_code_precedence(cls) -> types.ModuleType: 53 | """ 54 | Verify that specific status codes are always checked first, then ranges, then default 55 | """ 56 | return status_code_precedence 57 | -------------------------------------------------------------------------------- /tests/test_parser/test_bodies.py: -------------------------------------------------------------------------------- 1 | from openapi_python_client import schema as oai 2 | from openapi_python_client.parser.bodies import body_from_data 3 | from openapi_python_client.parser.errors import ParseError 4 | from openapi_python_client.parser.properties import Schemas 5 | 6 | 7 | def test_errors(config): 8 | operation = oai.Operation( 9 | requestBody=oai.RequestBody( 10 | content={ 11 | "invalid content type": oai.MediaType( 12 | media_type_schema=oai.Schema( 13 | type=oai.DataType.STRING, 14 | ) 15 | ), 16 | "application/json": oai.MediaType( 17 | media_type_schema=None # Missing media type schema is an error 18 | ), 19 | "text/html": oai.MediaType( # content type not supported by the generator 20 | media_type_schema=oai.Schema( 21 | type=oai.DataType.STRING, 22 | ) 23 | ), 24 | "application/sushi+json": oai.MediaType( 25 | media_type_schema=oai.Schema( 26 | type=oai.DataType.INTEGER, 27 | default="make this an invalid property", 28 | ) 29 | ), 30 | } 31 | ), 32 | responses={}, 33 | ) 34 | 35 | errs, _ = body_from_data( 36 | data=operation, schemas=Schemas(), config=config, endpoint_name="this will not succeed", request_bodies={} 37 | ) 38 | 39 | assert len(errs) == len(operation.request_body.content) 40 | assert all(isinstance(err, ParseError) for err in errs) 41 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="ModelWithAnyJsonPropertiesAdditionalPropertyType0") 10 | 11 | 12 | @_attrs_define 13 | class ModelWithAnyJsonPropertiesAdditionalPropertyType0: 14 | """ """ 15 | 16 | additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | model_with_any_json_properties_additional_property_type_0 = cls() 28 | 29 | model_with_any_json_properties_additional_property_type_0.additional_properties = d 30 | return model_with_any_json_properties_additional_property_type_0 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> str: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: str) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="PostResponsesUnionsSimpleBeforeComplexResponse200AType1") 10 | 11 | 12 | @_attrs_define 13 | class PostResponsesUnionsSimpleBeforeComplexResponse200AType1: 14 | """ """ 15 | 16 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 17 | 18 | def to_dict(self) -> dict[str, Any]: 19 | field_dict: dict[str, Any] = {} 20 | field_dict.update(self.additional_properties) 21 | 22 | return field_dict 23 | 24 | @classmethod 25 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 26 | d = dict(src_dict) 27 | post_responses_unions_simple_before_complex_response_200a_type_1 = cls() 28 | 29 | post_responses_unions_simple_before_complex_response_200a_type_1.additional_properties = d 30 | return post_responses_unions_simple_before_complex_response_200a_type_1 31 | 32 | @property 33 | def additional_keys(self) -> list[str]: 34 | return list(self.additional_properties.keys()) 35 | 36 | def __getitem__(self, key: str) -> Any: 37 | return self.additional_properties[key] 38 | 39 | def __setitem__(self, key: str, value: Any) -> None: 40 | self.additional_properties[key] = value 41 | 42 | def __delitem__(self, key: str) -> None: 43 | del self.additional_properties[key] 44 | 45 | def __contains__(self, key: str) -> bool: 46 | return key in self.additional_properties 47 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/json_like_body.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="JsonLikeBody") 12 | 13 | 14 | @_attrs_define 15 | class JsonLikeBody: 16 | """ 17 | Attributes: 18 | a (str | Unset): 19 | """ 20 | 21 | a: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | a = self.a 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if a is not UNSET: 31 | field_dict["a"] = a 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | a = d.pop("a", UNSET) 39 | 40 | json_like_body = cls( 41 | a=a, 42 | ) 43 | 44 | json_like_body.additional_properties = d 45 | return json_like_body 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/open_api.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict, field_validator 2 | 3 | from .components import Components 4 | from .external_documentation import ExternalDocumentation 5 | from .info import Info 6 | from .paths import Paths 7 | from .security_requirement import SecurityRequirement 8 | from .server import Server 9 | from .tag import Tag 10 | 11 | NUM_SEMVER_PARTS = 3 12 | 13 | 14 | class OpenAPI(BaseModel): 15 | """This is the root document object of the OpenAPI document. 16 | 17 | References: 18 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject 19 | - https://swagger.io/docs/specification/basic-structure/ 20 | """ 21 | 22 | info: Info 23 | servers: list[Server] = [Server(url="/")] 24 | paths: Paths 25 | components: Components | None = None 26 | security: list[SecurityRequirement] | None = None 27 | tags: list[Tag] | None = None 28 | externalDocs: ExternalDocumentation | None = None 29 | openapi: str 30 | model_config = ConfigDict( 31 | # `Components` is not build yet, will rebuild in `__init__.py`: 32 | defer_build=True, 33 | extra="allow", 34 | ) 35 | 36 | @field_validator("openapi") 37 | @classmethod 38 | def check_openapi_version(cls, value: str) -> str: 39 | """Validates that the declared OpenAPI version is a supported one""" 40 | parts = value.split(".") 41 | if len(parts) != NUM_SEMVER_PARTS: 42 | raise ValueError(f"Invalid OpenAPI version {value}") 43 | if parts[0] != "3": 44 | raise ValueError(f"Only OpenAPI versions 3.* are supported, got {value}") 45 | if int(parts[1]) > 1: 46 | raise ValueError(f"Only OpenAPI versions 3.1.* are supported, got {value}") 47 | return value 48 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/link.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | from .server import Server 6 | 7 | 8 | class Link(BaseModel): 9 | """ 10 | The `Link object` represents a possible design-time link for a response. 11 | The presence of a link does not guarantee the caller's ability to successfully invoke it, 12 | rather it provides a known relationship and traversal mechanism between responses and other operations. 13 | 14 | Unlike _dynamic_ links (i.e. links provided **in** the response payload), 15 | the OAS linking mechanism does not require link information in the runtime response. 16 | 17 | For computing links, and providing instructions to execute them, 18 | a [runtime expression](#runtimeExpression) is used for accessing values in an operation 19 | and using them as parameters while invoking the linked operation. 20 | 21 | References: 22 | - https://swagger.io/docs/specification/links/ 23 | - https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#linkObject 24 | """ 25 | 26 | operationRef: str | None = None 27 | operationId: str | None = None 28 | parameters: dict[str, Any] | None = None 29 | requestBody: Any | None = None 30 | description: str | None = None 31 | server: Server | None = None 32 | model_config = ConfigDict( 33 | extra="allow", 34 | json_schema_extra={ 35 | "examples": [ 36 | {"operationId": "getUserAddressByUUID", "parameters": {"userUuid": "$response.body#/uuid"}}, 37 | { 38 | "operationRef": "#/paths/~12.0~1repositories~1{username}/get", 39 | "parameters": {"username": "$response.body#/username"}, 40 | }, 41 | ] 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import TYPE_CHECKING, Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | 8 | from ..types import UNSET, Unset 9 | 10 | if TYPE_CHECKING: 11 | from ..models.validation_error import ValidationError 12 | 13 | 14 | T = TypeVar("T", bound="HTTPValidationError") 15 | 16 | 17 | @_attrs_define 18 | class HTTPValidationError: 19 | """ 20 | Attributes: 21 | detail (list[ValidationError] | Unset): 22 | """ 23 | 24 | detail: list[ValidationError] | Unset = UNSET 25 | 26 | def to_dict(self) -> dict[str, Any]: 27 | detail: list[dict[str, Any]] | Unset = UNSET 28 | if not isinstance(self.detail, Unset): 29 | detail = [] 30 | for detail_item_data in self.detail: 31 | detail_item = detail_item_data.to_dict() 32 | detail.append(detail_item) 33 | 34 | field_dict: dict[str, Any] = {} 35 | 36 | field_dict.update({}) 37 | if detail is not UNSET: 38 | field_dict["detail"] = detail 39 | 40 | return field_dict 41 | 42 | @classmethod 43 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 44 | from ..models.validation_error import ValidationError 45 | 46 | d = dict(src_dict) 47 | _detail = d.pop("detail", UNSET) 48 | detail: list[ValidationError] | Unset = UNSET 49 | if _detail is not UNSET: 50 | detail = [] 51 | for detail_item_data in _detail: 52 | detail_item = ValidationError.from_dict(detail_item_data) 53 | 54 | detail.append(detail_item) 55 | 56 | http_validation_error = cls( 57 | detail=detail, 58 | ) 59 | 60 | return http_validation_error 61 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_data_body.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="PostBodiesMultipleDataBody") 12 | 13 | 14 | @_attrs_define 15 | class PostBodiesMultipleDataBody: 16 | """ 17 | Attributes: 18 | a (str | Unset): 19 | """ 20 | 21 | a: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | a = self.a 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if a is not UNSET: 31 | field_dict["a"] = a 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | a = d.pop("a", UNSET) 39 | 40 | post_bodies_multiple_data_body = cls( 41 | a=a, 42 | ) 43 | 44 | post_bodies_multiple_data_body.additional_properties = d 45 | return post_bodies_multiple_data_body 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_json_body.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="PostBodiesMultipleJsonBody") 12 | 13 | 14 | @_attrs_define 15 | class PostBodiesMultipleJsonBody: 16 | """ 17 | Attributes: 18 | a (str | Unset): 19 | """ 20 | 21 | a: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | a = self.a 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if a is not UNSET: 31 | field_dict["a"] = a 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | a = d.pop("a", UNSET) 39 | 40 | post_bodies_multiple_json_body = cls( 41 | a=a, 42 | ) 43 | 44 | post_bodies_multiple_json_body.additional_properties = d 45 | return post_bodies_multiple_json_body 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/status_code_patterns_response_4xx.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="StatusCodePatternsResponse4XX") 12 | 13 | 14 | @_attrs_define 15 | class StatusCodePatternsResponse4XX: 16 | """ 17 | Attributes: 18 | error (str | Unset): 19 | """ 20 | 21 | error: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | error = self.error 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if error is not UNSET: 31 | field_dict["error"] = error 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | error = d.pop("error", UNSET) 39 | 40 | status_code_patterns_response_4xx = cls( 41 | error=error, 42 | ) 43 | 44 | status_code_patterns_response_4xx.additional_properties = d 45 | return status_code_patterns_response_4xx 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/models/an_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="AnObject") 10 | 11 | 12 | @_attrs_define 13 | class AnObject: 14 | """ 15 | Attributes: 16 | an_int (int): 17 | a_float (float): 18 | """ 19 | 20 | an_int: int 21 | a_float: float 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | an_int = self.an_int 26 | 27 | a_float = self.a_float 28 | 29 | field_dict: dict[str, Any] = {} 30 | field_dict.update(self.additional_properties) 31 | field_dict.update( 32 | { 33 | "an_int": an_int, 34 | "a_float": a_float, 35 | } 36 | ) 37 | 38 | return field_dict 39 | 40 | @classmethod 41 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 42 | d = dict(src_dict) 43 | an_int = d.pop("an_int") 44 | 45 | a_float = d.pop("a_float") 46 | 47 | an_object = cls( 48 | an_int=an_int, 49 | a_float=a_float, 50 | ) 51 | 52 | an_object.additional_properties = d 53 | return an_object 54 | 55 | @property 56 | def additional_keys(self) -> list[str]: 57 | return list(self.additional_properties.keys()) 58 | 59 | def __getitem__(self, key: str) -> Any: 60 | return self.additional_properties[key] 61 | 62 | def __setitem__(self, key: str, value: Any) -> None: 63 | self.additional_properties[key] = value 64 | 65 | def __delitem__(self, key: str) -> None: 66 | del self.additional_properties[key] 67 | 68 | def __contains__(self, key: str) -> bool: 69 | return key in self.additional_properties 70 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..models.an_enum import AnEnum 10 | 11 | T = TypeVar("T", bound="ModelWithAdditionalPropertiesRefed") 12 | 13 | 14 | @_attrs_define 15 | class ModelWithAdditionalPropertiesRefed: 16 | """ """ 17 | 18 | additional_properties: dict[str, AnEnum] = _attrs_field(init=False, factory=dict) 19 | 20 | def to_dict(self) -> dict[str, Any]: 21 | field_dict: dict[str, Any] = {} 22 | for prop_name, prop in self.additional_properties.items(): 23 | field_dict[prop_name] = prop.value 24 | 25 | return field_dict 26 | 27 | @classmethod 28 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 29 | d = dict(src_dict) 30 | model_with_additional_properties_refed = cls() 31 | 32 | additional_properties = {} 33 | for prop_name, prop_dict in d.items(): 34 | additional_property = AnEnum(prop_dict) 35 | 36 | additional_properties[prop_name] = additional_property 37 | 38 | model_with_additional_properties_refed.additional_properties = additional_properties 39 | return model_with_additional_properties_refed 40 | 41 | @property 42 | def additional_keys(self) -> list[str]: 43 | return list(self.additional_properties.keys()) 44 | 45 | def __getitem__(self, key: str) -> AnEnum: 46 | return self.additional_properties[key] 47 | 48 | def __setitem__(self, key: str, value: AnEnum) -> None: 49 | self.additional_properties[key] = value 50 | 51 | def __delitem__(self, key: str) -> None: 52 | del self.additional_properties[key] 53 | 54 | def __contains__(self, key: str) -> bool: 55 | return key in self.additional_properties 56 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_1.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="ADiscriminatedUnionType1") 12 | 13 | 14 | @_attrs_define 15 | class ADiscriminatedUnionType1: 16 | """ 17 | Attributes: 18 | model_type (str | Unset): 19 | """ 20 | 21 | model_type: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | model_type = self.model_type 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if model_type is not UNSET: 31 | field_dict["modelType"] = model_type 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | model_type = d.pop("modelType", UNSET) 39 | 40 | a_discriminated_union_type_1 = cls( 41 | model_type=model_type, 42 | ) 43 | 44 | a_discriminated_union_type_1.additional_properties = d 45 | return a_discriminated_union_type_1 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="ADiscriminatedUnionType2") 12 | 13 | 14 | @_attrs_define 15 | class ADiscriminatedUnionType2: 16 | """ 17 | Attributes: 18 | model_type (str | Unset): 19 | """ 20 | 21 | model_type: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | model_type = self.model_type 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if model_type is not UNSET: 31 | field_dict["modelType"] = model_type 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | model_type = d.pop("modelType", UNSET) 39 | 40 | a_discriminated_union_type_2 = cls( 41 | model_type=model_type, 42 | ) 43 | 44 | a_discriminated_union_type_2.additional_properties = d 45 | return a_discriminated_union_type_2 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /tests/test_parser/test_properties/test_protocol.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from openapi_python_client.parser.properties import AnyProperty 6 | from openapi_python_client.parser.properties.protocol import Value 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "required,no_optional,json,expected", 11 | [ 12 | (False, False, False, "TestType | Unset"), 13 | (False, True, False, "TestType"), 14 | (True, False, False, "TestType"), 15 | (True, True, False, "TestType"), 16 | (False, False, True, "str | Unset"), 17 | (False, True, True, "str"), 18 | (True, False, True, "str"), 19 | (True, True, True, "str"), 20 | ], 21 | ) 22 | def test_get_type_string(any_property_factory, mocker, required, no_optional, json, expected): 23 | mocker.patch.object(AnyProperty, "_type_string", "TestType") 24 | mocker.patch.object(AnyProperty, "_json_type_string", "str") 25 | p = any_property_factory(required=required) 26 | assert p.get_type_string(no_optional=no_optional, json=json) == expected 27 | 28 | 29 | @pytest.mark.parametrize( 30 | "default,required,expected", 31 | [ 32 | (None, False, "test: Any | Unset = UNSET"), 33 | (None, True, "test: Any"), 34 | ("Test", False, "test: Any | Unset = Test"), 35 | ("Test", True, "test: Any = Test"), 36 | ], 37 | ) 38 | def test_to_string(default: str | None, required: bool, expected: str, any_property_factory): 39 | name = "test" 40 | p = any_property_factory( 41 | name=name, required=required, default=Value(default, default) if default is not None else None 42 | ) 43 | 44 | assert p.to_string() == expected 45 | 46 | 47 | def test_get_imports(any_property_factory): 48 | p = any_property_factory() 49 | assert p.get_imports(prefix="") == set() 50 | 51 | p = any_property_factory(name="test", required=False, default=None) 52 | assert p.get_imports(prefix="") == {"from types import UNSET, Unset"} 53 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeOptionalObject") 10 | 11 | 12 | @_attrs_define 13 | class BodyUploadFileTestsUploadPostSomeOptionalObject: 14 | """ 15 | Attributes: 16 | foo (str): 17 | """ 18 | 19 | foo: str 20 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 21 | 22 | def to_dict(self) -> dict[str, Any]: 23 | foo = self.foo 24 | 25 | field_dict: dict[str, Any] = {} 26 | field_dict.update(self.additional_properties) 27 | field_dict.update( 28 | { 29 | "foo": foo, 30 | } 31 | ) 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | foo = d.pop("foo") 39 | 40 | body_upload_file_tests_upload_post_some_optional_object = cls( 41 | foo=foo, 42 | ) 43 | 44 | body_upload_file_tests_upload_post_some_optional_object.additional_properties = d 45 | return body_upload_file_tests_upload_post_some_optional_object 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_apples.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="ModelWithUnionPropertyInlinedApples") 12 | 13 | 14 | @_attrs_define 15 | class ModelWithUnionPropertyInlinedApples: 16 | """ 17 | Attributes: 18 | apples (str | Unset): 19 | """ 20 | 21 | apples: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | apples = self.apples 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if apples is not UNSET: 31 | field_dict["apples"] = apples 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | apples = d.pop("apples", UNSET) 39 | 40 | model_with_union_property_inlined_apples = cls( 41 | apples=apples, 42 | ) 43 | 44 | model_with_union_property_inlined_apples.additional_properties = d 45 | return model_with_union_property_inlined_apples 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /openapi_python_client/parser/properties/none.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, ClassVar 4 | 5 | from attr import define 6 | 7 | from ... import schema as oai 8 | from ...utils import PythonIdentifier 9 | from ..errors import PropertyError 10 | from .protocol import PropertyProtocol, Value 11 | 12 | 13 | @define 14 | class NoneProperty(PropertyProtocol): 15 | """A property that can only be None""" 16 | 17 | name: str 18 | required: bool 19 | default: Value | None 20 | python_name: PythonIdentifier 21 | description: str | None 22 | example: str | None 23 | 24 | _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { 25 | oai.ParameterLocation.QUERY, 26 | oai.ParameterLocation.COOKIE, 27 | oai.ParameterLocation.HEADER, 28 | } 29 | _type_string: ClassVar[str] = "None" 30 | _json_type_string: ClassVar[str] = "None" 31 | 32 | @classmethod 33 | def build( 34 | cls, 35 | name: str, 36 | required: bool, 37 | default: Any, 38 | python_name: PythonIdentifier, 39 | description: str | None, 40 | example: str | None, 41 | ) -> NoneProperty | PropertyError: 42 | checked_default = cls.convert_value(default) 43 | if isinstance(checked_default, PropertyError): 44 | return checked_default 45 | return cls( 46 | name=name, 47 | required=required, 48 | default=checked_default, 49 | python_name=python_name, 50 | description=description, 51 | example=example, 52 | ) 53 | 54 | @classmethod 55 | def convert_value(cls, value: Any) -> Value | None | PropertyError: 56 | if value is None or isinstance(value, Value): 57 | return value 58 | if isinstance(value, str): 59 | if value == "None": 60 | return Value(python_code=value, raw_value=value) 61 | return PropertyError(f"Value {value} is not valid, only None is allowed") 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_bananas.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="ModelWithUnionPropertyInlinedBananas") 12 | 13 | 14 | @_attrs_define 15 | class ModelWithUnionPropertyInlinedBananas: 16 | """ 17 | Attributes: 18 | bananas (str | Unset): 19 | """ 20 | 21 | bananas: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | bananas = self.bananas 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if bananas is not UNSET: 31 | field_dict["bananas"] = bananas 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | bananas = d.pop("bananas", UNSET) 39 | 40 | model_with_union_property_inlined_bananas = cls( 41 | bananas=bananas, 42 | ) 43 | 44 | model_with_union_property_inlined_bananas.additional_properties = d 45 | return model_with_union_property_inlined_bananas 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict, Field 2 | 3 | from .oauth_flows import OAuthFlows 4 | 5 | 6 | class SecurityScheme(BaseModel): 7 | """ 8 | Defines a security scheme that can be used by the operations. 9 | Supported schemes are HTTP authentication, 10 | an API key (either as a header, a cookie parameter or as a query parameter), 11 | OAuth2's common flows (implicit, password, client credentials and authorization code) 12 | as defined in [RFC6749](https://tools.ietf.org/html/rfc6749), 13 | and [OpenID Connect Discovery](https://tools.ietf.org/html/draft-ietf-oauth-discovery-06). 14 | 15 | References: 16 | - https://swagger.io/docs/specification/authentication/ 17 | - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject 18 | """ 19 | 20 | type: str 21 | description: str | None = None 22 | name: str | None = None 23 | security_scheme_in: str | None = Field(default=None, alias="in") 24 | scheme: str | None = None 25 | bearerFormat: str | None = None 26 | flows: OAuthFlows | None = None 27 | openIdConnectUrl: str | None = None 28 | model_config = ConfigDict( 29 | extra="allow", 30 | populate_by_name=True, 31 | json_schema_extra={ 32 | "examples": [ 33 | {"type": "http", "scheme": "basic"}, 34 | {"type": "apiKey", "name": "api_key", "in": "header"}, 35 | {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}, 36 | { 37 | "type": "oauth2", 38 | "flows": { 39 | "implicit": { 40 | "authorizationUrl": "https://example.com/api/oauth/dialog", 41 | "scopes": {"write:pets": "modify pets in your account", "read:pets": "read your pets"}, 42 | } 43 | }, 44 | }, 45 | ] 46 | }, 47 | ) 48 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="BodyUploadFileTestsUploadPostAdditionalProperty") 12 | 13 | 14 | @_attrs_define 15 | class BodyUploadFileTestsUploadPostAdditionalProperty: 16 | """ 17 | Attributes: 18 | foo (str | Unset): 19 | """ 20 | 21 | foo: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | foo = self.foo 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if foo is not UNSET: 31 | field_dict["foo"] = foo 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | foo = d.pop("foo", UNSET) 39 | 40 | body_upload_file_tests_upload_post_additional_property = cls( 41 | foo=foo, 42 | ) 43 | 44 | body_upload_file_tests_upload_post_additional_property.additional_properties = d 45 | return body_upload_file_tests_upload_post_additional_property 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeNullableObject") 12 | 13 | 14 | @_attrs_define 15 | class BodyUploadFileTestsUploadPostSomeNullableObject: 16 | """ 17 | Attributes: 18 | bar (str | Unset): 19 | """ 20 | 21 | bar: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | bar = self.bar 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if bar is not UNSET: 31 | field_dict["bar"] = bar 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | bar = d.pop("bar", UNSET) 39 | 40 | body_upload_file_tests_upload_post_some_nullable_object = cls( 41 | bar=bar, 42 | ) 43 | 44 | body_upload_file_tests_upload_post_some_nullable_object.additional_properties = d 45 | return body_upload_file_tests_upload_post_some_nullable_object 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeObject") 10 | 11 | 12 | @_attrs_define 13 | class BodyUploadFileTestsUploadPostSomeObject: 14 | """ 15 | Attributes: 16 | num (float): 17 | text (str): 18 | """ 19 | 20 | num: float 21 | text: str 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | num = self.num 26 | 27 | text = self.text 28 | 29 | field_dict: dict[str, Any] = {} 30 | field_dict.update(self.additional_properties) 31 | field_dict.update( 32 | { 33 | "num": num, 34 | "text": text, 35 | } 36 | ) 37 | 38 | return field_dict 39 | 40 | @classmethod 41 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 42 | d = dict(src_dict) 43 | num = d.pop("num") 44 | 45 | text = d.pop("text") 46 | 47 | body_upload_file_tests_upload_post_some_object = cls( 48 | num=num, 49 | text=text, 50 | ) 51 | 52 | body_upload_file_tests_upload_post_some_object.additional_properties = d 53 | return body_upload_file_tests_upload_post_some_object 54 | 55 | @property 56 | def additional_keys(self) -> list[str]: 57 | return list(self.additional_properties.keys()) 58 | 59 | def __getitem__(self, key: str) -> Any: 60 | return self.additional_properties[key] 61 | 62 | def __setitem__(self, key: str, value: Any) -> None: 63 | self.additional_properties[key] = value 64 | 65 | def __delitem__(self, key: str) -> None: 66 | del self.additional_properties[key] 67 | 68 | def __contains__(self, key: str) -> bool: 69 | return key in self.additional_properties 70 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | from collections.abc import Mapping 5 | from typing import Any, TypeVar 6 | 7 | from attrs import define as _attrs_define 8 | from attrs import field as _attrs_field 9 | from dateutil.parser import isoparse 10 | 11 | T = TypeVar("T", bound="ModelWithPrimitiveAdditionalPropertiesADateHolder") 12 | 13 | 14 | @_attrs_define 15 | class ModelWithPrimitiveAdditionalPropertiesADateHolder: 16 | """ """ 17 | 18 | additional_properties: dict[str, datetime.datetime] = _attrs_field(init=False, factory=dict) 19 | 20 | def to_dict(self) -> dict[str, Any]: 21 | field_dict: dict[str, Any] = {} 22 | for prop_name, prop in self.additional_properties.items(): 23 | field_dict[prop_name] = prop.isoformat() 24 | 25 | return field_dict 26 | 27 | @classmethod 28 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 29 | d = dict(src_dict) 30 | model_with_primitive_additional_properties_a_date_holder = cls() 31 | 32 | additional_properties = {} 33 | for prop_name, prop_dict in d.items(): 34 | additional_property = isoparse(prop_dict) 35 | 36 | additional_properties[prop_name] = additional_property 37 | 38 | model_with_primitive_additional_properties_a_date_holder.additional_properties = additional_properties 39 | return model_with_primitive_additional_properties_a_date_holder 40 | 41 | @property 42 | def additional_keys(self) -> list[str]: 43 | return list(self.additional_properties.keys()) 44 | 45 | def __getitem__(self, key: str) -> datetime.datetime: 46 | return self.additional_properties[key] 47 | 48 | def __setitem__(self, key: str, value: datetime.datetime) -> None: 49 | self.additional_properties[key] = value 50 | 51 | def __delitem__(self, key: str) -> None: 52 | del self.additional_properties[key] 53 | 54 | def __contains__(self, key: str) -> bool: 55 | return key in self.additional_properties 56 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | T = TypeVar("T", bound="ModelWithRecursiveRefInAdditionalProperties") 10 | 11 | 12 | @_attrs_define 13 | class ModelWithRecursiveRefInAdditionalProperties: 14 | """ """ 15 | 16 | additional_properties: dict[str, ModelWithRecursiveRefInAdditionalProperties] = _attrs_field( 17 | init=False, factory=dict 18 | ) 19 | 20 | def to_dict(self) -> dict[str, Any]: 21 | field_dict: dict[str, Any] = {} 22 | for prop_name, prop in self.additional_properties.items(): 23 | field_dict[prop_name] = prop.to_dict() 24 | 25 | return field_dict 26 | 27 | @classmethod 28 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 29 | d = dict(src_dict) 30 | model_with_recursive_ref_in_additional_properties = cls() 31 | 32 | additional_properties = {} 33 | for prop_name, prop_dict in d.items(): 34 | additional_property = ModelWithRecursiveRefInAdditionalProperties.from_dict(prop_dict) 35 | 36 | additional_properties[prop_name] = additional_property 37 | 38 | model_with_recursive_ref_in_additional_properties.additional_properties = additional_properties 39 | return model_with_recursive_ref_in_additional_properties 40 | 41 | @property 42 | def additional_keys(self) -> list[str]: 43 | return list(self.additional_properties.keys()) 44 | 45 | def __getitem__(self, key: str) -> ModelWithRecursiveRefInAdditionalProperties: 46 | return self.additional_properties[key] 47 | 48 | def __setitem__(self, key: str, value: ModelWithRecursiveRefInAdditionalProperties) -> None: 49 | self.additional_properties[key] = value 50 | 51 | def __delitem__(self, key: str) -> None: 52 | del self.additional_properties[key] 53 | 54 | def __contains__(self, key: str) -> bool: 55 | return key in self.additional_properties 56 | -------------------------------------------------------------------------------- /openapi_python_client/schema/openapi_schema_pydantic/README.md: -------------------------------------------------------------------------------- 1 | Everything in this directory (including the rest of this file after this paragraph) is a vendored copy of [openapi-schem-pydantic](https://github.com/kuimono/openapi-schema-pydantic) and is licensed under the LICENSE file in this directory. 2 | 3 | Included vendored version is the [following](https://github.com/kuimono/openapi-schema-pydantic/commit/0836b429086917feeb973de3367a7ac4c2b3a665) 4 | Small patches has been applied to it. 5 | 6 | ## Alias 7 | 8 | Due to the reserved words in python and pydantic, 9 | the following fields are used with [alias](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) feature provided by pydantic: 10 | 11 | | Class | Field name in the class | Alias (as in OpenAPI spec) | 12 | | ----- | ----------------------- | -------------------------- | 13 | | Header[*](#header_param_in) | param_in | in | 14 | | MediaType | media_type_schema | schema | 15 | | Parameter | param_in | in | 16 | | Parameter | param_schema | schema | 17 | | PathItem | ref | $ref | 18 | | Reference | ref | $ref | 19 | | SecurityScheme | security_scheme_in | in | 20 | | Schema | schema_format | format | 21 | | Schema | schema_not | not | 22 | 23 | > The "in" field in Header object is actually a constant (`{"in": "header"}`). 24 | 25 | > For convenience of object creation, the classes mentioned in above 26 | > has configured `allow_population_by_field_name=True`. 27 | > 28 | > Reference: [Pydantic's Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/) 29 | 30 | ## Non-pydantic schema types 31 | 32 | Due to the constriants of python typing structure (not able to handle dynamic field names), 33 | the following schema classes are actually just a typing of `Dict`: 34 | 35 | | Schema Type | Implementation | 36 | | ----------- | -------------- | 37 | | Callback | `Callback = Dict[str, PathItem]` | 38 | | Paths | `Paths = Dict[str, PathItem]` | 39 | | Responses | `Responses = Dict[str, Union[Response, Reference]]` | 40 | | SecurityRequirement | `SecurityRequirement = Dict[str, List[str]]` | 41 | 42 | On creating such schema instances, please use python's `dict` type instead to instantiate. 43 | -------------------------------------------------------------------------------- /openapi_python_client/parser/properties/string.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, ClassVar, overload 4 | 5 | from attr import define 6 | 7 | from ... import schema as oai 8 | from ... import utils 9 | from ...utils import PythonIdentifier 10 | from ..errors import PropertyError 11 | from .protocol import PropertyProtocol, Value 12 | 13 | 14 | @define 15 | class StringProperty(PropertyProtocol): 16 | """A property of type str""" 17 | 18 | name: str 19 | required: bool 20 | default: Value | None 21 | python_name: PythonIdentifier 22 | description: str | None 23 | example: str | None 24 | _type_string: ClassVar[str] = "str" 25 | _json_type_string: ClassVar[str] = "str" 26 | _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { 27 | oai.ParameterLocation.QUERY, 28 | oai.ParameterLocation.PATH, 29 | oai.ParameterLocation.COOKIE, 30 | oai.ParameterLocation.HEADER, 31 | } 32 | 33 | @classmethod 34 | def build( 35 | cls, 36 | name: str, 37 | required: bool, 38 | default: Any, 39 | python_name: PythonIdentifier, 40 | description: str | None, 41 | example: str | None, 42 | ) -> StringProperty | PropertyError: 43 | checked_default = cls.convert_value(default) 44 | return cls( 45 | name=name, 46 | required=required, 47 | default=checked_default, 48 | python_name=python_name, 49 | description=description, 50 | example=example, 51 | ) 52 | 53 | @classmethod 54 | @overload 55 | def convert_value(cls, value: None) -> None: # type: ignore[misc] 56 | ... # pragma: no cover 57 | 58 | @classmethod 59 | @overload 60 | def convert_value(cls, value: Any) -> Value: ... # pragma: no cover 61 | 62 | @classmethod 63 | def convert_value(cls, value: Any) -> Value | None: 64 | if value is None or isinstance(value, Value): 65 | return value 66 | if not isinstance(value, str): 67 | value = str(value) 68 | return Value(python_code=repr(utils.remove_string_escapes(value)), raw_value=value) 69 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | 8 | from ..models.an_enum import AnEnum 9 | from ..models.an_int_enum import AnIntEnum 10 | from ..types import UNSET, Unset 11 | 12 | T = TypeVar("T", bound="ModelWithUnionProperty") 13 | 14 | 15 | @_attrs_define 16 | class ModelWithUnionProperty: 17 | """ 18 | Attributes: 19 | a_property (AnEnum | AnIntEnum | Unset): 20 | """ 21 | 22 | a_property: AnEnum | AnIntEnum | Unset = UNSET 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | a_property: int | str | Unset 26 | if isinstance(self.a_property, Unset): 27 | a_property = UNSET 28 | elif isinstance(self.a_property, AnEnum): 29 | a_property = self.a_property.value 30 | else: 31 | a_property = self.a_property.value 32 | 33 | field_dict: dict[str, Any] = {} 34 | 35 | field_dict.update({}) 36 | if a_property is not UNSET: 37 | field_dict["a_property"] = a_property 38 | 39 | return field_dict 40 | 41 | @classmethod 42 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 43 | d = dict(src_dict) 44 | 45 | def _parse_a_property(data: object) -> AnEnum | AnIntEnum | Unset: 46 | if isinstance(data, Unset): 47 | return data 48 | try: 49 | if not isinstance(data, str): 50 | raise TypeError() 51 | a_property_type_0 = AnEnum(data) 52 | 53 | return a_property_type_0 54 | except (TypeError, ValueError, AttributeError, KeyError): 55 | pass 56 | if not isinstance(data, int): 57 | raise TypeError() 58 | a_property_type_1 = AnIntEnum(data) 59 | 60 | return a_property_type_1 61 | 62 | a_property = _parse_a_property(d.pop("a_property", UNSET)) 63 | 64 | model_with_union_property = cls( 65 | a_property=a_property, 66 | ) 67 | 68 | return model_with_union_property 69 | -------------------------------------------------------------------------------- /integration-tests/integration_tests/models/problem.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="Problem") 12 | 13 | 14 | @_attrs_define 15 | class Problem: 16 | """ 17 | Attributes: 18 | parameter_name (str | Unset): 19 | description (str | Unset): 20 | """ 21 | 22 | parameter_name: str | Unset = UNSET 23 | description: str | Unset = UNSET 24 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 25 | 26 | def to_dict(self) -> dict[str, Any]: 27 | parameter_name = self.parameter_name 28 | 29 | description = self.description 30 | 31 | field_dict: dict[str, Any] = {} 32 | field_dict.update(self.additional_properties) 33 | field_dict.update({}) 34 | if parameter_name is not UNSET: 35 | field_dict["parameter_name"] = parameter_name 36 | if description is not UNSET: 37 | field_dict["description"] = description 38 | 39 | return field_dict 40 | 41 | @classmethod 42 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 43 | d = dict(src_dict) 44 | parameter_name = d.pop("parameter_name", UNSET) 45 | 46 | description = d.pop("description", UNSET) 47 | 48 | problem = cls( 49 | parameter_name=parameter_name, 50 | description=description, 51 | ) 52 | 53 | problem.additional_properties = d 54 | return problem 55 | 56 | @property 57 | def additional_keys(self) -> list[str]: 58 | return list(self.additional_properties.keys()) 59 | 60 | def __getitem__(self, key: str) -> Any: 61 | return self.additional_properties[key] 62 | 63 | def __setitem__(self, key: str, value: Any) -> None: 64 | self.additional_properties[key] = value 65 | 66 | def __delitem__(self, key: str) -> None: 67 | del self.additional_properties[key] 68 | 69 | def __contains__(self, key: str) -> bool: 70 | return key in self.additional_properties 71 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="ModelWithAdditionalPropertiesInlinedAdditionalProperty") 12 | 13 | 14 | @_attrs_define 15 | class ModelWithAdditionalPropertiesInlinedAdditionalProperty: 16 | """ 17 | Attributes: 18 | extra_props_prop (str | Unset): 19 | """ 20 | 21 | extra_props_prop: str | Unset = UNSET 22 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 23 | 24 | def to_dict(self) -> dict[str, Any]: 25 | extra_props_prop = self.extra_props_prop 26 | 27 | field_dict: dict[str, Any] = {} 28 | field_dict.update(self.additional_properties) 29 | field_dict.update({}) 30 | if extra_props_prop is not UNSET: 31 | field_dict["extra_props_prop"] = extra_props_prop 32 | 33 | return field_dict 34 | 35 | @classmethod 36 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 37 | d = dict(src_dict) 38 | extra_props_prop = d.pop("extra_props_prop", UNSET) 39 | 40 | model_with_additional_properties_inlined_additional_property = cls( 41 | extra_props_prop=extra_props_prop, 42 | ) 43 | 44 | model_with_additional_properties_inlined_additional_property.additional_properties = d 45 | return model_with_additional_properties_inlined_additional_property 46 | 47 | @property 48 | def additional_keys(self) -> list[str]: 49 | return list(self.additional_properties.keys()) 50 | 51 | def __getitem__(self, key: str) -> Any: 52 | return self.additional_properties[key] 53 | 54 | def __setitem__(self, key: str, value: Any) -> None: 55 | self.additional_properties[key] = value 56 | 57 | def __delitem__(self, key: str) -> None: 58 | del self.additional_properties[key] 59 | 60 | def __contains__(self, key: str) -> bool: 61 | return key in self.additional_properties 62 | -------------------------------------------------------------------------------- /end_to_end_tests/golden-record/my_test_api_client/models/mixed_case_response_200.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping 4 | from typing import Any, TypeVar 5 | 6 | from attrs import define as _attrs_define 7 | from attrs import field as _attrs_field 8 | 9 | from ..types import UNSET, Unset 10 | 11 | T = TypeVar("T", bound="MixedCaseResponse200") 12 | 13 | 14 | @_attrs_define 15 | class MixedCaseResponse200: 16 | """ 17 | Attributes: 18 | mixed_case (str | Unset): 19 | mixedCase (str | Unset): 20 | """ 21 | 22 | mixed_case: str | Unset = UNSET 23 | mixedCase: str | Unset = UNSET 24 | additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) 25 | 26 | def to_dict(self) -> dict[str, Any]: 27 | mixed_case = self.mixed_case 28 | 29 | mixedCase = self.mixedCase 30 | 31 | field_dict: dict[str, Any] = {} 32 | field_dict.update(self.additional_properties) 33 | field_dict.update({}) 34 | if mixed_case is not UNSET: 35 | field_dict["mixed_case"] = mixed_case 36 | if mixedCase is not UNSET: 37 | field_dict["mixedCase"] = mixedCase 38 | 39 | return field_dict 40 | 41 | @classmethod 42 | def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: 43 | d = dict(src_dict) 44 | mixed_case = d.pop("mixed_case", UNSET) 45 | 46 | mixedCase = d.pop("mixedCase", UNSET) 47 | 48 | mixed_case_response_200 = cls( 49 | mixed_case=mixed_case, 50 | mixedCase=mixedCase, 51 | ) 52 | 53 | mixed_case_response_200.additional_properties = d 54 | return mixed_case_response_200 55 | 56 | @property 57 | def additional_keys(self) -> list[str]: 58 | return list(self.additional_properties.keys()) 59 | 60 | def __getitem__(self, key: str) -> Any: 61 | return self.additional_properties[key] 62 | 63 | def __setitem__(self, key: str, value: Any) -> None: 64 | self.additional_properties[key] = value 65 | 66 | def __delitem__(self, key: str) -> None: 67 | del self.additional_properties[key] 68 | 69 | def __contains__(self, key: str) -> bool: 70 | return key in self.additional_properties 71 | --------------------------------------------------------------------------------