@typespec/http-client-python 0.14.2 → 0.14.3-dev.1

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 (38) hide show
  1. package/dist/emitter/emitter.js +1 -3
  2. package/dist/emitter/emitter.js.map +1 -1
  3. package/dist/emitter/lib.d.ts +1 -0
  4. package/dist/emitter/lib.d.ts.map +1 -1
  5. package/dist/emitter/lib.js +10 -0
  6. package/dist/emitter/lib.js.map +1 -1
  7. package/emitter/src/emitter.ts +1 -3
  8. package/emitter/src/lib.ts +13 -0
  9. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  10. package/eng/scripts/ci/regenerate.ts +11 -4
  11. package/eng/scripts/setup/__pycache__/package_manager.cpython-39.pyc +0 -0
  12. package/eng/scripts/setup/__pycache__/venvtools.cpython-39.pyc +0 -0
  13. package/generator/build/lib/pygen/__init__.py +19 -1
  14. package/generator/build/lib/pygen/codegen/models/code_model.py +32 -0
  15. package/generator/build/lib/pygen/codegen/models/operation.py +3 -4
  16. package/generator/build/lib/pygen/codegen/models/paging_operation.py +1 -1
  17. package/generator/build/lib/pygen/codegen/serializers/__init__.py +77 -73
  18. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +4 -3
  19. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +86 -13
  20. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +109 -0
  21. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  22. package/generator/pygen/__init__.py +19 -1
  23. package/generator/pygen/codegen/models/code_model.py +32 -0
  24. package/generator/pygen/codegen/models/operation.py +3 -4
  25. package/generator/pygen/codegen/models/paging_operation.py +1 -1
  26. package/generator/pygen/codegen/serializers/__init__.py +77 -73
  27. package/generator/pygen/codegen/serializers/builder_serializer.py +4 -3
  28. package/generator/pygen/codegen/serializers/general_serializer.py +86 -13
  29. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +109 -0
  30. package/generator/pygen.egg-info/SOURCES.txt +1 -0
  31. package/generator/test/azure/requirements.txt +1 -0
  32. package/generator/test/azure/tox.ini +2 -2
  33. package/generator/test/generic_mock_api_tests/asynctests/test_authentication_async.py +7 -4
  34. package/generator/test/generic_mock_api_tests/test_authentication.py +7 -4
  35. package/generator/test/unbranded/mock_api_tests/test_unbranded.py +3 -1
  36. package/generator/test/unbranded/requirements.txt +1 -0
  37. package/generator/test/unbranded/tox.ini +2 -2
  38. package/package.json +1 -1
@@ -4,7 +4,10 @@
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
6
  import json
7
- from typing import Any, List
7
+ from typing import Any, List, TYPE_CHECKING
8
+ import re
9
+ import tomli as tomllib
10
+ from packaging.version import parse as parse_version
8
11
  from .import_serializer import FileImportSerializer, TypingSection
9
12
  from ..models.imports import MsrestImportType, FileImport
10
13
  from ..models import (
@@ -16,6 +19,9 @@ from ..models.utils import NamespaceType
16
19
  from .client_serializer import ClientSerializer, ConfigSerializer
17
20
  from .base_serializer import BaseSerializer
18
21
 
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
19
25
  VERSION_MAP = {
20
26
  "msrest": "0.7.1",
21
27
  "isodate": "0.6.1",
@@ -42,8 +48,73 @@ class GeneralSerializer(BaseSerializer):
42
48
  params.update({"options": self.code_model.options})
43
49
  return template.render(code_model=self.code_model, **params)
44
50
 
45
- def serialize_package_file(self, template_name: str, **kwargs: Any) -> str:
51
+ def _extract_min_dependency(self, s):
52
+ # Extract the minimum version from a dependency string.
53
+ #
54
+ # Handles formats like:
55
+ # - >=1.2.3
56
+ # - >=0.1.0b1 (beta versions)
57
+ # - >=1.2.3rc2 (release candidates)
58
+ #
59
+ # Returns the parsed version if found, otherwise version "0".
60
+ m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s)
61
+ return parse_version(m.group(1)) if m else parse_version("0")
62
+
63
+ def _keep_pyproject_fields(self, file_path: "Path") -> dict:
64
+ # Load the pyproject.toml file if it exists and extract fields to keep.
65
+ result: dict = {"KEEP_FIELDS": {}}
66
+ try:
67
+ with open(file_path, "rb") as f:
68
+ loaded_pyproject_toml = tomllib.load(f)
69
+ except Exception: # pylint: disable=broad-except
70
+ # If parsing the pyproject.toml fails, we assume the it does not exist or is incorrectly formatted.
71
+ return result
72
+
73
+ # Keep azure-sdk-build configuration
74
+ if "tool" in loaded_pyproject_toml and "azure-sdk-build" in loaded_pyproject_toml["tool"]:
75
+ result["KEEP_FIELDS"]["tool.azure-sdk-build"] = loaded_pyproject_toml["tool"]["azure-sdk-build"]
76
+
77
+ # Process dependencies
78
+ if "project" in loaded_pyproject_toml:
79
+ # Handle main dependencies
80
+ if "dependencies" in loaded_pyproject_toml["project"]:
81
+ kept_deps = []
82
+ for dep in loaded_pyproject_toml["project"]["dependencies"]:
83
+ dep_name = re.split(r"[<>=\[]", dep)[0].strip()
84
+
85
+ # Check if dependency is one we track in VERSION_MAP
86
+ if dep_name in VERSION_MAP:
87
+ # For tracked dependencies, check if the version is higher than our default
88
+ default_version = parse_version(VERSION_MAP[dep_name])
89
+ dep_version = self._extract_min_dependency(dep)
90
+ # If the version is higher than the default, update VERSION_MAP
91
+ # with higher min dependency version
92
+ if dep_version > default_version:
93
+ VERSION_MAP[dep_name] = str(dep_version)
94
+ else:
95
+ # Keep non-default dependencies
96
+ kept_deps.append(dep)
97
+
98
+ if kept_deps:
99
+ result["KEEP_FIELDS"]["project.dependencies"] = kept_deps
100
+
101
+ # Keep optional dependencies
102
+ if "optional-dependencies" in loaded_pyproject_toml["project"]:
103
+ result["KEEP_FIELDS"]["project.optional-dependencies"] = loaded_pyproject_toml["project"][
104
+ "optional-dependencies"
105
+ ]
106
+
107
+ return result
108
+
109
+ def serialize_package_file(self, template_name: str, file_path: "Path", **kwargs: Any) -> str:
46
110
  template = self.env.get_template(template_name)
111
+
112
+ # Add fields to keep from an existing pyproject.toml
113
+ if template_name == "pyproject.toml.jinja2":
114
+ params = self._keep_pyproject_fields(file_path)
115
+ else:
116
+ params = {}
117
+
47
118
  package_parts = (
48
119
  self.code_model.namespace.split(".")[:-1]
49
120
  if self.code_model.is_tsp
@@ -57,17 +128,19 @@ class GeneralSerializer(BaseSerializer):
57
128
  dev_status = "4 - Beta"
58
129
  else:
59
130
  dev_status = "5 - Production/Stable"
60
- params = {
61
- "code_model": self.code_model,
62
- "dev_status": dev_status,
63
- "token_credential": token_credential,
64
- "pkgutil_names": [".".join(package_parts[: i + 1]) for i in range(len(package_parts))],
65
- "init_names": ["/".join(package_parts[: i + 1]) + "/__init__.py" for i in range(len(package_parts))],
66
- "client_name": self.code_model.clients[0].name if self.code_model.clients else "",
67
- "VERSION_MAP": VERSION_MAP,
68
- "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION,
69
- "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION,
70
- }
131
+ params.update(
132
+ {
133
+ "code_model": self.code_model,
134
+ "dev_status": dev_status,
135
+ "token_credential": token_credential,
136
+ "pkgutil_names": [".".join(package_parts[: i + 1]) for i in range(len(package_parts))],
137
+ "init_names": ["/".join(package_parts[: i + 1]) + "/__init__.py" for i in range(len(package_parts))],
138
+ "client_name": self.code_model.clients[0].name if self.code_model.clients else "",
139
+ "VERSION_MAP": VERSION_MAP,
140
+ "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION,
141
+ "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION,
142
+ }
143
+ )
71
144
  params.update({"options": self.code_model.options})
72
145
  params.update(kwargs)
73
146
  return template.render(file_import=FileImport(self.code_model), **params)
@@ -0,0 +1,109 @@
1
+ {% set min_version = MIN_PYTHON_VERSION.split('.')[1] | int %}
2
+ {% set max_version = MAX_PYTHON_VERSION.split('.')[1] | int %}
3
+ {% if code_model.license_header %}
4
+ {{ code_model.license_header }}
5
+ {% endif %}
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=61.0.0", "wheel"] # Requires 61.0.0 for dynamic version
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "{{ options.get('package-name')|lower }}"
13
+ {% if options.get('package-mode') %}
14
+ authors = [
15
+ { name = "{{ code_model.company_name }}"{% if code_model.is_azure_flavor %}, email = "azpysdkhelp@microsoft.com"{% endif %} },
16
+ ]
17
+ description = "{{ code_model.company_name }} {% if code_model.is_azure_flavor and not options.get('package-pprint-name').startswith('Azure ') %}Azure {% endif %}{{ options.get('package-pprint-name') }} Client Library for Python"
18
+ license = {text = "MIT License"}
19
+ classifiers = [
20
+ "Development Status :: {{ dev_status }}",
21
+ "Programming Language :: Python",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3",
24
+ {% for version in range(min_version, max_version + 1) %}
25
+ "Programming Language :: Python :: 3.{{ version }}",
26
+ {% endfor %}
27
+ "License :: OSI Approved :: MIT License",
28
+ ]
29
+ requires-python = ">={{ MIN_PYTHON_VERSION }}"
30
+ {% else %}
31
+ description = "{{ options.get('package-name') }}"
32
+ {% endif %}
33
+ {% if code_model.is_azure_flavor %}
34
+ keywords = ["azure", "azure sdk"]
35
+ {% endif %}
36
+
37
+ dependencies = [
38
+ {% if code_model.is_legacy %}
39
+ "msrest>={{ VERSION_MAP['msrest'] }}",
40
+ {% else %}
41
+ "isodate>={{ VERSION_MAP['isodate'] }}",
42
+ {% endif %}
43
+ {% if options.get('azure_arm') %}
44
+ "azure-mgmt-core>={{ VERSION_MAP['azure-mgmt-core'] }}",
45
+ {% elif code_model.is_azure_flavor %}
46
+ "azure-core>={{ VERSION_MAP['azure-core'] }}",
47
+ {% else %}
48
+ "corehttp[requests]>={{ VERSION_MAP['corehttp'] }}",
49
+ {% endif %}
50
+ "typing-extensions>={{ VERSION_MAP['typing-extensions'] }}",
51
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.dependencies') %}
52
+ {% for dep in KEEP_FIELDS.get('project.dependencies') %}
53
+ "{{ dep }}",
54
+ {% endfor %}
55
+ {% endif %}
56
+ ]
57
+ dynamic = [
58
+ {% if options.get('package-mode') %}"version", {% endif %}"readme"
59
+ ]
60
+ {% if not options.get('package-mode') %}
61
+ version = "{{ options.get("package-version", "unknown") }}"
62
+ {% endif %}
63
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.optional-dependencies') %}
64
+
65
+ [project.optional-dependencies]
66
+ {% for key, val in KEEP_FIELDS.get('project.optional-dependencies').items() %}
67
+ {{ key }} = [
68
+ {% for dep in val %}
69
+ "{{ dep }}",
70
+ {% endfor %}
71
+ ]
72
+ {% endfor %}
73
+ {% endif %}
74
+ {% if code_model.is_azure_flavor %}
75
+
76
+ [project.urls]
77
+ repository = "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk"
78
+ {% endif %}
79
+
80
+ [tool.setuptools.dynamic]
81
+ {% if options.get('package-mode') %}
82
+ {% if code_model.is_tsp %}
83
+ version = {attr = "{{ code_model.namespace|lower }}._version.VERSION"}
84
+ {% else %}
85
+ version = {attr = "{{ options.get('package-name')|lower|replace('-', '.') }}._version.VERSION"}
86
+ {% endif %}
87
+ {% endif %}
88
+ readme = {file = ["README.md"], content-type = "text/markdown"}
89
+ {% if options.get('package-mode') %}
90
+
91
+ [tool.setuptools.packages.find]
92
+ exclude = [
93
+ "tests*",
94
+ "samples*",
95
+ {% for pkgutil_name in pkgutil_names %}
96
+ "{{ pkgutil_name }}",
97
+ {% endfor %}
98
+ ]
99
+
100
+ [tool.setuptools.package-data]
101
+ pytyped = ["py.typed"]
102
+ {% endif %}
103
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('tool.azure-sdk-build') %}
104
+
105
+ [tool.azure-sdk-build]
106
+ {% for key, val in KEEP_FIELDS.get('tool.azure-sdk-build').items() %}
107
+ {{ key }} = {{ val|tojson }}
108
+ {% endfor %}
109
+ {% endif %}
@@ -25,10 +25,10 @@ class OptionsDict(MutableMapping):
25
25
  "azure-arm": False,
26
26
  "basic-setup-py": False,
27
27
  "client-side-validation": False,
28
- "emit-cross-language-definition-file": False,
29
28
  "flavor": "azure", # need to default to azure in shared code so we don't break swagger generation
30
29
  "from-typespec": False,
31
30
  "generate-sample": False,
31
+ "keep-setup-py": False,
32
32
  "generate-test": False,
33
33
  "head-as-boolean": True,
34
34
  "keep-version-file": False,
@@ -39,6 +39,7 @@ class OptionsDict(MutableMapping):
39
39
  "polymorphic-examples": 5,
40
40
  "validate-versioning": True,
41
41
  "version-tolerant": True,
42
+ "generation-subdir": None, # subdirectory to generate the code in
42
43
  }
43
44
 
44
45
  def __init__(self, options: Optional[Dict[str, Any]] = None) -> None:
@@ -106,6 +107,8 @@ class OptionsDict(MutableMapping):
106
107
  models_mode_default = "dpg"
107
108
  # switch to falsy value for easier code writing
108
109
  return models_mode_default
110
+ if key == "emit-cross-language-definition-file":
111
+ return self.get("flavor") == "azure"
109
112
  return self.DEFAULTS[key]
110
113
 
111
114
  def _validate_combinations(self) -> None:
@@ -134,6 +137,10 @@ class OptionsDict(MutableMapping):
134
137
  "We are working on creating a new multiapi SDK for version tolerant and it is not available yet."
135
138
  )
136
139
 
140
+ # If multiapi, do not generate default pyproject.toml
141
+ if self.get("multiapi"):
142
+ self["keep-setup-py"] = True
143
+
137
144
  if self.get("client-side-validation") and self.get("version-tolerant"):
138
145
  raise ValueError("Can not generate version tolerant with --client-side-validation. ")
139
146
 
@@ -210,6 +217,9 @@ class ReaderAndWriter:
210
217
  _LOGGER.warning("Loading python.json file. This behavior will be depreacted")
211
218
  self.options.update(python_json)
212
219
 
220
+ def get_output_folder(self) -> Path:
221
+ return self.output_folder
222
+
213
223
  def read_file(self, path: Union[str, Path]) -> str:
214
224
  """Directly reading from disk"""
215
225
  # make path relative to output folder
@@ -227,6 +237,14 @@ class ReaderAndWriter:
227
237
  with open(self.output_folder / Path(filename), "w", encoding="utf-8") as fd:
228
238
  fd.write(file_content)
229
239
 
240
+ def remove_file(self, filename: Union[str, Path]) -> None:
241
+ try:
242
+ file_path = self.output_folder / Path(filename)
243
+ if file_path.is_file():
244
+ file_path.unlink()
245
+ except FileNotFoundError:
246
+ pass
247
+
230
248
  def list_file(self) -> List[str]:
231
249
  return [str(f.relative_to(self.output_folder)) for f in self.output_folder.glob("**/*") if f.is_file()]
232
250
 
@@ -3,6 +3,7 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
+ from pathlib import Path
6
7
  from typing import List, Dict, Any, Set, Union, Literal, Optional, cast
7
8
 
8
9
  from .base import BaseType
@@ -452,3 +453,34 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
452
453
  return self.yaml_data.get("licenseInfo", {}).get("company", "")
453
454
  # typespec azure case without custom license and swagger case
454
455
  return "Microsoft Corporation"
456
+
457
+ def get_root_dir(self) -> Path:
458
+ if self.options["no-namespace-folders"]:
459
+ # when output folder contains parts different from the namespace, we fall back to current folder directly.
460
+ return Path(".")
461
+ return Path(*self.namespace.split("."))
462
+
463
+ def get_generation_dir(self, namespace: str) -> Path:
464
+ """The directory to generate the code in.
465
+
466
+ If 'generation-subdir' is specified, it will be used as a subdirectory.
467
+ """
468
+ root_dir = self.get_root_dir()
469
+ retval = self._get_relative_generation_dir(root_dir, namespace)
470
+ return retval
471
+
472
+ def _get_relative_generation_dir(self, root_dir: Path, namespace: str) -> Path:
473
+ if self.options["no-namespace-folders"]:
474
+ return Path(".")
475
+ if self.options.get("generation-subdir"):
476
+ # For the main namespace, return root_dir + generation-subdir
477
+ if namespace in ("", self.namespace):
478
+ return root_dir / self.options["generation-subdir"]
479
+
480
+ # For subnamespaces, extract the subnamespace part and append it to generation-subdir
481
+ if namespace.startswith(self.namespace + "."):
482
+ subnamespace_parts = namespace[len(self.namespace) + 1 :].split(".")
483
+ return root_dir / self.options["generation-subdir"] / Path(*subnamespace_parts)
484
+
485
+ # No generation-subdir specified, use the namespace path directly
486
+ return Path(*namespace.split("."))
@@ -194,14 +194,13 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
194
194
  def any_response_has_headers(self) -> bool:
195
195
  return any(response.headers for response in self.responses)
196
196
 
197
- @property
198
- def default_error_deserialization(self) -> Optional[str]:
197
+ def default_error_deserialization(self, serialize_namespace: str) -> Optional[str]:
199
198
  default_exceptions = [e for e in self.exceptions if "default" in e.status_codes and e.type]
200
199
  if not default_exceptions:
201
200
  return None
202
201
  exception_schema = default_exceptions[0].type
203
202
  if isinstance(exception_schema, ModelType):
204
- return exception_schema.type_annotation(skip_quote=True)
203
+ return exception_schema.type_annotation(skip_quote=True, serialize_namespace=serialize_namespace)
205
204
  return None if self.code_model.options["models-mode"] == "dpg" else "'object'"
206
205
 
207
206
  @property
@@ -469,7 +468,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
469
468
  file_import.add_submodule_import(relative_path, "_deserialize_xml", ImportType.LOCAL)
470
469
  elif self.need_deserialize:
471
470
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
472
- if self.default_error_deserialization or self.non_default_errors:
471
+ if self.default_error_deserialization(serialize_namespace) or self.non_default_errors:
473
472
  file_import.add_submodule_import(relative_path, "_failsafe_deserialize", ImportType.LOCAL)
474
473
  return file_import
475
474
 
@@ -173,7 +173,7 @@ class PagingOperationBase(OperationBase[PagingResponseType]):
173
173
  serialize_namespace, module_name="_utils.model_base"
174
174
  )
175
175
  file_import.merge(self.item_type.imports(**kwargs))
176
- if self.default_error_deserialization or self.need_deserialize:
176
+ if self.default_error_deserialization(serialize_namespace) or self.need_deserialize:
177
177
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
178
178
  return file_import
179
179