openapi-python-client 0.29.0__tar.gz → 0.29.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/.gitignore +2 -4
  2. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/PKG-INFO +4 -5
  3. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/__init__.py +26 -17
  4. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/cli.py +1 -2
  5. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/config.py +3 -1
  6. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/bodies.py +8 -7
  7. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/openapi.py +58 -48
  8. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/__init__.py +34 -33
  9. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/any.py +11 -10
  10. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/boolean.py +13 -13
  11. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/const.py +11 -10
  12. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/date.py +12 -11
  13. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/datetime.py +12 -11
  14. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/enum_property.py +23 -20
  15. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/file.py +11 -10
  16. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/float.py +13 -13
  17. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/int.py +11 -11
  18. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/list_property.py +22 -22
  19. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/literal_enum_property.py +20 -20
  20. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/merge_properties.py +9 -6
  21. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/model_property.py +34 -33
  22. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/none.py +11 -11
  23. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/protocol.py +42 -32
  24. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/schemas.py +30 -23
  25. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/string.py +10 -11
  26. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/union.py +24 -22
  27. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/uuid.py +11 -11
  28. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/responses.py +30 -23
  29. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/__init__.py +2 -1
  30. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/callback.py +3 -1
  31. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/components.py +10 -9
  32. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/contact.py +5 -3
  33. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +4 -2
  34. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +4 -3
  35. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/example.py +5 -3
  36. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +4 -2
  37. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/header.py +20 -6
  38. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/info.py +5 -4
  39. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/license.py +4 -2
  40. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/link.py +5 -4
  41. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +3 -2
  42. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +6 -4
  43. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +5 -3
  44. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/operation.py +6 -5
  45. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +6 -5
  46. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +5 -3
  47. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/paths.py +2 -1
  48. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/reference.py +3 -1
  49. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +3 -2
  50. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/response.py +5 -4
  51. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/responses.py +2 -1
  52. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/schema.py +7 -6
  53. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +3 -1
  54. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +8 -7
  55. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/server.py +4 -3
  56. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +5 -3
  57. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/tag.py +3 -2
  58. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/xml.py +5 -3
  59. openapi_python_client-0.29.1/openapi_python_client/schema/ref.py +52 -0
  60. openapi_python_client-0.29.1/openapi_python_client/schema/untrusted_string.py +56 -0
  61. openapi_python_client-0.29.1/openapi_python_client/strings.py +245 -0
  62. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/README.md.jinja +3 -3
  63. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/client.py.jinja +5 -5
  64. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/endpoint_macros.py.jinja +18 -18
  65. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/endpoint_module.py.jinja +13 -13
  66. openapi_python_client-0.29.1/openapi_python_client/templates/helpers.jinja +6 -0
  67. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/literal_enum.py.jinja +1 -1
  68. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/model.py.jinja +18 -18
  69. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/package_init.py.jinja +2 -2
  70. openapi_python_client-0.29.1/openapi_python_client/templates/property_templates/const_property.py.jinja +9 -0
  71. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/date_property.py.jinja +1 -1
  72. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/datetime_property.py.jinja +1 -1
  73. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/enum_property.py.jinja +1 -1
  74. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/file_property.py.jinja +1 -1
  75. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/list_property.py.jinja +3 -3
  76. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +2 -2
  77. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/model_property.py.jinja +1 -1
  78. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/property_macros.py.jinja +1 -1
  79. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/union_property.py.jinja +9 -9
  80. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/uuid_property.py.jinja +1 -1
  81. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/pyproject_pdm.toml.jinja +2 -2
  82. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/pyproject_poetry.toml.jinja +2 -2
  83. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/pyproject_uv.toml.jinja +3 -3
  84. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/setup.py.jinja +2 -2
  85. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/str_enum.py.jinja +2 -2
  86. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/pyproject.toml +22 -20
  87. openapi_python_client-0.29.0/openapi_python_client/templates/helpers.jinja +0 -10
  88. openapi_python_client-0.29.0/openapi_python_client/templates/property_templates/const_property.py.jinja +0 -9
  89. openapi_python_client-0.29.0/openapi_python_client/utils.py +0 -122
  90. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/LICENSE +0 -0
  91. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/README.md +0 -0
  92. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/__main__.py +0 -0
  93. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/__init__.py +0 -0
  94. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/errors.py +0 -0
  95. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/parser/properties/property.py +0 -0
  96. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/py.typed +0 -0
  97. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/3.0.3.md +0 -0
  98. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/3.1.0.md +0 -0
  99. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/data_type.py +0 -0
  100. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/LICENSE +0 -0
  101. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/README.md +0 -0
  102. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/__init__.py +0 -0
  103. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py +0 -0
  104. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/schema/parameter_location.py +0 -0
  105. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/.gitignore.jinja +0 -0
  106. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/api_init.py.jinja +0 -0
  107. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/endpoint_init.py.jinja +0 -0
  108. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/errors.py.jinja +0 -0
  109. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/int_enum.py.jinja +0 -0
  110. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/models_init.py.jinja +0 -0
  111. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/any_property.py.jinja +0 -0
  112. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/boolean_property.py.jinja +0 -0
  113. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/float_property.py.jinja +0 -0
  114. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/helpers.jinja +0 -0
  115. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/property_templates/int_property.py.jinja +0 -0
  116. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/pyproject.toml.jinja +0 -0
  117. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/pyproject_ruff.toml.jinja +0 -0
  118. {openapi_python_client-0.29.0 → openapi_python_client-0.29.1}/openapi_python_client/templates/types.py.jinja +0 -0
@@ -34,7 +34,5 @@ test-reports/
34
34
  htmlcov/
35
35
 
36
36
  # Generated end to end test data
37
- my-test-api-client/
38
- custom-e2e/
39
- 3-1-features-client
40
- tests/tmp
37
+ tests/tmp
38
+ end_to_end_tests/tmp/*
@@ -1,15 +1,14 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: openapi-python-client
3
- Version: 0.29.0
3
+ Version: 0.29.1
4
4
  Summary: Generate modern Python clients from OpenAPI
5
5
  Project-URL: repository, https://github.com/openapi-generators/openapi-python-client
6
6
  Author-email: Dylan Anthony <contact@dylananthony.com>
7
- License: MIT
7
+ License-Expression: MIT
8
8
  License-File: LICENSE
9
9
  Keywords: Client,Generator,OpenAPI
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
12
  Classifier: Programming Language :: Python :: 3
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
@@ -26,7 +25,7 @@ Requires-Dist: pydantic<3.0.0,>=2.10
26
25
  Requires-Dist: ruamel-yaml<0.20.0,>=0.18.6
27
26
  Requires-Dist: ruff>=0.2
28
27
  Requires-Dist: shellingham<2.0.0,>=1.3.2
29
- Requires-Dist: typer<0.27,>0.16
28
+ Requires-Dist: typer<0.28,>0.16
30
29
  Description-Content-Type: text/markdown
31
30
 
32
31
  ![Run Checks](https://github.com/openapi-generators/openapi-python-client/workflows/Run%20Checks/badge.svg)
@@ -4,7 +4,7 @@ import json
4
4
  import mimetypes
5
5
  import shutil
6
6
  import subprocess
7
- from collections.abc import Sequence
7
+ from collections.abc import Callable, Sequence
8
8
  from importlib.metadata import version
9
9
  from pathlib import Path
10
10
  from subprocess import CalledProcessError
@@ -16,7 +16,7 @@ from jinja2 import BaseLoader, ChoiceLoader, Environment, FileSystemLoader, Pack
16
16
  from ruamel.yaml import YAML
17
17
  from ruamel.yaml.error import YAMLError
18
18
 
19
- from openapi_python_client import utils
19
+ from openapi_python_client import strings
20
20
 
21
21
  from .config import Config, MetaType
22
22
  from .parser import GeneratorData, import_string_from_class
@@ -25,11 +25,16 @@ from .parser.properties import LiteralEnumProperty
25
25
 
26
26
  __version__ = version(__package__)
27
27
 
28
+ from .schema.untrusted_string import UntrustedString
28
29
 
29
- TEMPLATE_FILTERS = {
30
- "snakecase": utils.snake_case,
31
- "kebabcase": utils.kebab_case,
32
- "pascalcase": utils.pascal_case,
30
+ TEMPLATE_FILTERS: dict[str, Callable[..., object]] = {
31
+ "snakecase": strings.snake_case,
32
+ "kebabcase": strings.kebab_case,
33
+ "pascalcase": strings.pascal_case,
34
+ "safe_for_docstring": strings.safe_for_docstring,
35
+ "in_double_quote_literal": strings.in_double_quote_literal,
36
+ "in_f_string_literal": strings.in_f_string_literal,
37
+ "as_unembedded_code": strings.PythonCode.as_unembedded_code,
33
38
  "any": any,
34
39
  }
35
40
 
@@ -66,14 +71,14 @@ class Project:
66
71
  keep_trailing_newline=True,
67
72
  )
68
73
 
69
- self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client"
74
+ self.project_name: str = config.project_name_override or f"{strings.kebab_case(openapi.title).lower()}-client"
70
75
  self.package_name: str = config.package_name_override or self.project_name.replace("-", "_")
71
76
  self.project_dir: Path # Where the generated code will be placed
72
77
  self.package_dir: Path # Where the generated Python module will be placed (same as project_dir if no meta)
73
78
 
74
79
  if config.output_path is not None:
75
80
  self.project_dir = config.output_path
76
- elif config.meta_type == MetaType.NONE:
81
+ elif config.meta_type == MetaType.NONE: # pragma: no cover
77
82
  self.project_dir = Path.cwd() / self.package_name
78
83
  else:
79
84
  self.project_dir = Path.cwd() / self.project_name
@@ -83,17 +88,20 @@ class Project:
83
88
  else:
84
89
  self.package_dir = self.project_dir / self.package_name
85
90
 
86
- self.package_description: str = utils.remove_string_escapes(
87
- f"A client library for accessing {self.openapi.title}"
91
+ self.package_description = UntrustedString(
92
+ f"A client library for accessing {self.openapi.title.get_untrusted_value()}"
93
+ )
94
+ self.version: UntrustedString = (
95
+ UntrustedString(config.package_version_override) if config.package_version_override else openapi.version
88
96
  )
89
- self.version: str = config.package_version_override or openapi.version
90
97
 
91
98
  self.env.filters.update(TEMPLATE_FILTERS)
92
99
  self.env.globals.update(
93
100
  config=config,
94
- utils=utils,
95
- python_identifier=lambda x: utils.PythonIdentifier(x, config.field_prefix),
96
- class_name=lambda x: utils.ClassName(x, config.field_prefix),
101
+ strings=strings,
102
+ PythonCode=strings.PythonCode,
103
+ python_identifier=lambda x: strings.PythonIdentifier(x, config.field_prefix),
104
+ class_name=lambda x: strings.ClassName(x, config.field_prefix),
97
105
  package_name=self.package_name,
98
106
  package_dir=self.package_dir,
99
107
  package_description=self.package_description,
@@ -110,7 +118,7 @@ class Project:
110
118
 
111
119
  print(f"Generating {self.project_dir}")
112
120
  try:
113
- self.project_dir.mkdir()
121
+ self.project_dir.mkdir(parents=True)
114
122
  except FileExistsError:
115
123
  if not self.config.overwrite:
116
124
  return [GeneratorError(detail="Directory already exists. Delete it or use the --overwrite option.")]
@@ -267,7 +275,8 @@ class Project:
267
275
 
268
276
  endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag
269
277
  endpoint_template = self.env.get_template(
270
- "endpoint_module.py.jinja", globals={"isbool": lambda obj: obj.get_base_type_string() == "bool"}
278
+ "endpoint_module.py.jinja",
279
+ globals={"isbool": lambda obj: obj.get_base_type_string() == strings.PythonCode("bool")},
271
280
  )
272
281
  for tag, collection in endpoint_collections_by_tag.items():
273
282
  tag_dir = api_dir / tag
@@ -281,7 +290,7 @@ class Project:
281
290
  )
282
291
 
283
292
  for endpoint in collection.endpoints:
284
- module_path = tag_dir / f"{utils.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py"
293
+ module_path = tag_dir / f"{strings.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py"
285
294
  module_path.write_text(
286
295
  endpoint_template.render(
287
296
  endpoint=endpoint,
@@ -1,7 +1,6 @@
1
1
  import codecs
2
2
  from collections.abc import Sequence
3
3
  from pathlib import Path
4
- from pprint import pformat
5
4
 
6
5
  import typer
7
6
 
@@ -75,7 +74,7 @@ def _print_parser_error(err: GeneratorError, color: str) -> None:
75
74
  typer.echo(err=True)
76
75
 
77
76
  if isinstance(err, ParseError) and err.data is not None:
78
- formatted_data = pformat(err.data)
77
+ formatted_data = err.data.model_dump_json(indent=2, exclude_unset=True, by_alias=True)
79
78
  typer.secho(formatted_data, fg=color, err=True)
80
79
 
81
80
  typer.echo(err=True)
@@ -7,6 +7,8 @@ from attr import define
7
7
  from pydantic import BaseModel
8
8
  from ruamel.yaml import YAML
9
9
 
10
+ from openapi_python_client.schema.untrusted_string import UntrustedString
11
+
10
12
 
11
13
  class ClassOverride(BaseModel):
12
14
  """An override of a single generated class.
@@ -14,7 +16,7 @@ class ClassOverride(BaseModel):
14
16
  See https://github.com/openapi-generators/openapi-python-client#class_overrides
15
17
  """
16
18
 
17
- class_name: str | None = None
19
+ class_name: UntrustedString | None = None
18
20
  module_name: str | None = None
19
21
 
20
22
 
@@ -12,7 +12,8 @@ from openapi_python_client.parser.properties.schemas import get_reference_simple
12
12
 
13
13
  from .. import schema as oai
14
14
  from ..config import Config
15
- from ..utils import get_content_type
15
+ from ..schema.untrusted_string import UntrustedString
16
+ from ..strings import get_content_type
16
17
  from .errors import ErrorLevel, ParseError
17
18
 
18
19
 
@@ -25,7 +26,7 @@ class BodyType(StrEnum):
25
26
 
26
27
  @attr.define
27
28
  class Body:
28
- content_type: str
29
+ content_type: UntrustedString
29
30
  prop: Property
30
31
  body_type: BodyType
31
32
 
@@ -34,7 +35,7 @@ def body_from_data(
34
35
  *,
35
36
  data: oai.Operation,
36
37
  schemas: Schemas,
37
- request_bodies: dict[str, oai.RequestBody | oai.Reference],
38
+ request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference],
38
39
  config: Config,
39
40
  endpoint_name: str,
40
41
  ) -> tuple[list[Body | ParseError], Schemas]:
@@ -50,7 +51,7 @@ def body_from_data(
50
51
  prefix_type_names = len(body_content) > 1
51
52
 
52
53
  for content_type, media_type in body_content.items():
53
- simplified_content_type = get_content_type(content_type, config)
54
+ simplified_content_type = get_content_type(content_type.get_untrusted_value(), config)
54
55
  if simplified_content_type is None:
55
56
  bodies.append(
56
57
  ParseError(
@@ -88,7 +89,7 @@ def body_from_data(
88
89
  )
89
90
  continue
90
91
  prop, schemas = property_from_data(
91
- name="body",
92
+ name=UntrustedString("body"),
92
93
  required=body.required,
93
94
  data=media_type_schema,
94
95
  schemas=schemas,
@@ -121,7 +122,7 @@ def body_from_data(
121
122
 
122
123
 
123
124
  def _resolve_reference(
124
- body: oai.RequestBody | oai.Reference | None, request_bodies: dict[str, oai.RequestBody | oai.Reference]
125
+ body: oai.RequestBody | oai.Reference | None, request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference]
125
126
  ) -> oai.RequestBody | ParseError | None:
126
127
  if body is None:
127
128
  return None
@@ -132,5 +133,5 @@ def _resolve_reference(
132
133
  if isinstance(body, oai.Reference):
133
134
  return ParseError(detail="Circular $ref in request body", data=body)
134
135
  if body is None and references_seen:
135
- return ParseError(detail=f"Could not resolve $ref {references_seen[-1]} in request body")
136
+ return ParseError(detail=f"Could not resolve $ref {references_seen[-1].get_untrusted_value()} in request body")
136
137
  return body
@@ -7,9 +7,10 @@ from typing import Any, Protocol
7
7
  from pydantic import ValidationError
8
8
 
9
9
  from .. import schema as oai
10
- from .. import utils
10
+ from .. import strings
11
11
  from ..config import Config
12
- from ..utils import PythonIdentifier
12
+ from ..schema.untrusted_string import UntrustedString
13
+ from ..strings import PythonIdentifier
13
14
  from .bodies import Body, body_from_data
14
15
  from .errors import GeneratorError, ParseError, PropertyError
15
16
  from .properties import (
@@ -46,15 +47,15 @@ class EndpointCollection:
46
47
  @staticmethod
47
48
  def from_data(
48
49
  *,
49
- data: dict[str, oai.PathItem],
50
+ data: dict[UntrustedString, oai.PathItem],
50
51
  schemas: Schemas,
51
52
  parameters: Parameters,
52
- request_bodies: dict[str, oai.RequestBody | oai.Reference],
53
- responses: dict[str, oai.Response | oai.Reference],
53
+ request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference],
54
+ responses: dict[UntrustedString, oai.Response | oai.Reference],
54
55
  config: Config,
55
- ) -> tuple[dict[utils.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]:
56
+ ) -> tuple[dict[strings.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]:
56
57
  """Parse the openapi paths data to get EndpointCollections by tag"""
57
- endpoints_by_tag: dict[utils.PythonIdentifier, EndpointCollection] = {}
58
+ endpoints_by_tag: dict[strings.PythonIdentifier, EndpointCollection] = {}
58
59
 
59
60
  methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
60
61
 
@@ -64,7 +65,10 @@ class EndpointCollection:
64
65
  if operation is None:
65
66
  continue
66
67
 
67
- tags = [utils.PythonIdentifier(value=tag, prefix="tag") for tag in operation.tags or ["default"]]
68
+ tags = [
69
+ strings.PythonIdentifier(value=tag, prefix="tag")
70
+ for tag in operation.tags or [UntrustedString("default")]
71
+ ]
68
72
  if not config.generate_all_tags:
69
73
  tags = tags[:1]
70
74
 
@@ -93,12 +97,14 @@ class EndpointCollection:
93
97
  if not isinstance(endpoint, ParseError):
94
98
  endpoint = Endpoint.sort_parameters(endpoint=endpoint)
95
99
  if isinstance(endpoint, ParseError):
96
- endpoint.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated."
100
+ endpoint.header = f"WARNING parsing {method.upper()} {path.get_untrusted_value()} within {'/'.join(tags)}. Endpoint will not be generated."
97
101
  for collection in collections:
98
102
  collection.parse_errors.append(endpoint)
99
103
  continue
100
104
  for error in endpoint.errors:
101
- error.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}."
105
+ error.header = (
106
+ f"WARNING parsing {method.upper()} {path.get_untrusted_value()} within {'/'.join(tags)}."
107
+ )
102
108
  for collection in collections:
103
109
  collection.parse_errors.append(error)
104
110
  for collection in collections:
@@ -107,14 +113,14 @@ class EndpointCollection:
107
113
  return endpoints_by_tag, schemas, parameters
108
114
 
109
115
 
110
- def generate_operation_id(*, path: str, method: str) -> str:
116
+ def generate_operation_id(*, path: UntrustedString, method: str, prefix: str) -> PythonIdentifier:
111
117
  """Generate an operationId from a path"""
112
- clean_path = path.replace("{", "").replace("}", "").replace("/", "_")
113
- if clean_path.startswith("_"):
114
- clean_path = clean_path[1:]
115
- if clean_path.endswith("_"):
116
- clean_path = clean_path[:-1]
117
- return f"{method}_{clean_path}"
118
+ unsafe_id = path.get_untrusted_value().replace("{", "").replace("}", "").replace("/", "_")
119
+ if unsafe_id.startswith("_"):
120
+ unsafe_id = unsafe_id[1:]
121
+ if unsafe_id.endswith("_"): # pragma: no cover
122
+ unsafe_id = unsafe_id[:-1]
123
+ return PythonIdentifier(f"{method}_{unsafe_id}", prefix=prefix)
118
124
 
119
125
 
120
126
  models_relative_prefix: str = "..."
@@ -134,13 +140,13 @@ class Endpoint:
134
140
  Describes a single endpoint on the server
135
141
  """
136
142
 
137
- path: str
143
+ path: UntrustedString
138
144
  method: str
139
- description: str | None
140
- name: str
145
+ description: UntrustedString
146
+ name: PythonIdentifier
141
147
  requires_security: bool
142
148
  tags: list[PythonIdentifier]
143
- summary: str | None = ""
149
+ summary: UntrustedString
144
150
  relative_imports: set[str] = field(default_factory=set)
145
151
  query_parameters: list[Property] = field(default_factory=list)
146
152
  path_parameters: list[Property] = field(default_factory=list)
@@ -156,7 +162,7 @@ class Endpoint:
156
162
  endpoint: "Endpoint",
157
163
  data: oai.Responses,
158
164
  schemas: Schemas,
159
- responses: dict[str, oai.Response | oai.Reference],
165
+ responses: dict[UntrustedString, oai.Response | oai.Reference],
160
166
  config: Config,
161
167
  ) -> tuple["Endpoint", Schemas]:
162
168
  endpoint = deepcopy(endpoint)
@@ -234,7 +240,7 @@ class Endpoint:
234
240
 
235
241
  endpoint = deepcopy(endpoint)
236
242
 
237
- unique_parameters: set[tuple[str, oai.ParameterLocation]] = set()
243
+ unique_parameters: set[tuple[UntrustedString, oai.ParameterLocation]] = set()
238
244
  parameters_by_location: dict[str, list[Property]] = {
239
245
  oai.ParameterLocation.QUERY: endpoint.query_parameters,
240
246
  oai.ParameterLocation.PATH: endpoint.path_parameters,
@@ -260,7 +266,7 @@ class Endpoint:
260
266
  detail=(
261
267
  "Parameters MUST NOT contain duplicates. "
262
268
  "A unique parameter is defined by a combination of a name and location. "
263
- f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`."
269
+ f"Duplicated parameters named `{param.name.get_untrusted_value()}` detected in `{param.param_in}`."
264
270
  ),
265
271
  ),
266
272
  schemas,
@@ -312,7 +318,7 @@ class Endpoint:
312
318
  self,
313
319
  *,
314
320
  config: Config,
315
- previously_modified_params: set[tuple[oai.ParameterLocation, str]] | None = None,
321
+ previously_modified_params: set[tuple[oai.ParameterLocation, UntrustedString]] | None = None,
316
322
  ) -> "Endpoint | ParseError":
317
323
  """Check for conflicting parameters
318
324
 
@@ -330,7 +336,7 @@ class Endpoint:
330
336
  location, prop = parameter
331
337
 
332
338
  if prop.python_name in reserved_names:
333
- prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config)
339
+ prop.set_python_name(new_name=UntrustedString(f"{prop.python_name}_{location}"), config=config)
334
340
  modified_params.add((location, prop.name))
335
341
  continue
336
342
 
@@ -349,9 +355,9 @@ class Endpoint:
349
355
 
350
356
  if location != conflicting_location:
351
357
  conflicting_prop.set_python_name(
352
- new_name=f"{conflicting_prop.python_name}_{conflicting_location}", config=config
358
+ new_name=UntrustedString(f"{conflicting_prop.python_name}_{conflicting_location}"), config=config
353
359
  )
354
- prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config)
360
+ prop.set_python_name(new_name=UntrustedString(f"{prop.python_name}_{location}"), config=config)
355
361
  elif conflicting_prop.name != prop.name: # Use the name to differentiate
356
362
  conflicting_prop.set_python_name(new_name=conflicting_prop.name, config=config, skip_snake_case=True)
357
363
  prop.set_python_name(new_name=prop.name, config=config, skip_snake_case=True)
@@ -378,7 +384,7 @@ class Endpoint:
378
384
  the path parameters and they could not be sorted.
379
385
  """
380
386
  endpoint = deepcopy(endpoint)
381
- parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path)
387
+ parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path.get_untrusted_value())
382
388
  try:
383
389
  endpoint.path_parameters.sort(
384
390
  key=lambda param: parameters_from_path.index(param.name),
@@ -388,37 +394,41 @@ class Endpoint:
388
394
 
389
395
  if parameters_from_path != [param.name for param in endpoint.path_parameters]:
390
396
  return ParseError(
391
- detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)",
397
+ detail=f"Incorrect path templating for {endpoint.path.get_untrusted_value()} (Path parameters do not match with path)",
392
398
  )
393
399
  for parameter in endpoint.path_parameters:
394
- endpoint.path = endpoint.path.replace(f"{{{parameter.name}}}", f"{{{parameter.python_name}}}")
400
+ endpoint.path = UntrustedString(
401
+ endpoint.path.get_untrusted_value().replace(
402
+ f"{{{parameter.name.get_untrusted_value()}}}", f"{{{parameter.python_name}}}"
403
+ )
404
+ )
395
405
  return endpoint
396
406
 
397
407
  @staticmethod
398
408
  def from_data(
399
409
  *,
400
410
  data: oai.Operation,
401
- path: str,
411
+ path: UntrustedString,
402
412
  method: str,
403
413
  tags: list[PythonIdentifier],
404
414
  schemas: Schemas,
405
415
  parameters: Parameters,
406
- request_bodies: dict[str, oai.RequestBody | oai.Reference],
407
- responses: dict[str, oai.Response | oai.Reference],
416
+ request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference],
417
+ responses: dict[UntrustedString, oai.Response | oai.Reference],
408
418
  config: Config,
409
419
  ) -> tuple["Endpoint | ParseError", Schemas, Parameters]:
410
420
  """Construct an endpoint from the OpenAPI data"""
411
421
 
412
422
  if data.operationId is None:
413
- name = generate_operation_id(path=path, method=method)
423
+ name = generate_operation_id(path=path, method=method, prefix=config.field_prefix)
414
424
  else:
415
- name = data.operationId
425
+ name = PythonIdentifier(data.operationId, prefix=config.field_prefix)
416
426
 
417
427
  endpoint = Endpoint(
418
428
  path=path,
419
429
  method=method,
420
- summary=utils.remove_string_escapes(data.summary) if data.summary else "",
421
- description=utils.remove_string_escapes(data.description) if data.description else "",
430
+ summary=data.summary if data.summary else UntrustedString(""),
431
+ description=data.description if data.description else UntrustedString(""),
422
432
  name=name,
423
433
  requires_security=bool(data.security),
424
434
  tags=tags,
@@ -440,7 +450,7 @@ class Endpoint:
440
450
  responses=responses,
441
451
  config=config,
442
452
  )
443
- if isinstance(result, ParseError):
453
+ if isinstance(result, ParseError): # pragma: no cover
444
454
  return result, schemas, parameters
445
455
  bodies, schemas = body_from_data(
446
456
  data=data, schemas=schemas, config=config, endpoint_name=result.name, request_bodies=request_bodies
@@ -467,14 +477,14 @@ class Endpoint:
467
477
 
468
478
  return result, schemas, parameters
469
479
 
470
- def response_type(self) -> str:
480
+ def response_type(self) -> strings.PythonCode:
471
481
  """Get the Python type of any response from this endpoint"""
472
- types = sorted({response.prop.get_type_string() for response in self.responses})
482
+ types = sorted({response.prop.get_type_string().as_unembedded_code() for response in self.responses})
473
483
  if len(types) == 0:
474
- return "Any"
484
+ return strings.PythonCode("Any")
475
485
  if len(types) == 1:
476
- return types[0]
477
- return " | ".join(types)
486
+ return strings.PythonCode(types[0])
487
+ return strings.PythonCode(" | ".join(types))
478
488
 
479
489
  def iter_all_parameters(self) -> Iterator[tuple[oai.ParameterLocation, Property]]:
480
490
  """Iterate through all the parameters of this endpoint"""
@@ -498,12 +508,12 @@ class Endpoint:
498
508
  class GeneratorData:
499
509
  """All the data needed to generate a client"""
500
510
 
501
- title: str
502
- description: str | None
503
- version: str
511
+ title: UntrustedString
512
+ description: UntrustedString | None
513
+ version: UntrustedString
504
514
  models: list[ModelProperty]
505
515
  errors: list[ParseError]
506
- endpoint_collections_by_tag: dict[utils.PythonIdentifier, EndpointCollection]
516
+ endpoint_collections_by_tag: dict[strings.PythonIdentifier, EndpointCollection]
507
517
  enums: list[EnumProperty | LiteralEnumProperty]
508
518
 
509
519
  @staticmethod