@typespec/http-client-python 0.29.1 → 0.30.0-alpha.20260529.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.
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +3 -2
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +3 -2
- package/dist/emitter/types.js.map +1 -1
- package/dist/emitter/utils.d.ts +5 -0
- package/dist/emitter/utils.d.ts.map +1 -1
- package/dist/emitter/utils.js +6 -2
- package/dist/emitter/utils.js.map +1 -1
- package/emitter/src/http.ts +3 -1
- package/emitter/src/types.ts +3 -1
- package/emitter/src/utils.ts +8 -2
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/Build-Packages.ps1 +17 -2
- package/eng/scripts/ci/dev_requirements.txt +2 -2
- package/eng/scripts/ci/regenerate-common.ts +21 -13
- package/eng/scripts/ci/run_pylint.py +9 -0
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/eng/scripts/setup/package_manager.py +13 -3
- package/generator/build/lib/pygen/codegen/models/operation.py +6 -1
- package/generator/build/lib/pygen/codegen/models/parameter.py +1 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +3 -2
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +113 -1
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +329 -45
- package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +22 -1
- package/generator/build/lib/pygen/preprocess/__init__.py +17 -5
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/codegen/models/operation.py +6 -1
- package/generator/pygen/codegen/models/parameter.py +1 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +3 -2
- package/generator/pygen/codegen/serializers/model_serializer.py +113 -1
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +329 -45
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +22 -1
- package/generator/pygen/preprocess/__init__.py +17 -5
- package/generator/pygen.egg-info/PKG-INFO +1 -1
- package/generator/pygen.egg-info/requires.txt +1 -1
- package/generator/setup.py +1 -1
- package/package.json +6 -6
|
@@ -75,6 +75,13 @@ New-Item -ItemType Directory -Force -Path "$outputPath/packages" | Out-Null
|
|
|
75
75
|
$emitterVersion = node -p -e "require('$packageRoot/package.json').version"
|
|
76
76
|
Write-Host "Package version: $emitterVersion"
|
|
77
77
|
|
|
78
|
+
# Stamp prerelease version if BuildNumber is provided
|
|
79
|
+
if ($BuildNumber) {
|
|
80
|
+
$versionTag = $Prerelease ? "-alpha" : "-beta"
|
|
81
|
+
$emitterVersion = "$emitterVersion$versionTag.$BuildNumber"
|
|
82
|
+
Write-Host "Stamped prerelease version: $emitterVersion"
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
Push-Location "$packageRoot"
|
|
79
86
|
try {
|
|
80
87
|
# Step 1: Build the emitter and generator
|
|
@@ -87,12 +94,20 @@ try {
|
|
|
87
94
|
Invoke-LoggedCommand "npm run lint" -GroupOutput
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
# Step 3:
|
|
97
|
+
# Step 3: Stamp version in package.json if prerelease
|
|
98
|
+
if ($BuildNumber) {
|
|
99
|
+
Write-Host "`n=== Updating package.json version to $emitterVersion ===" -ForegroundColor Cyan
|
|
100
|
+
$packageJson = Get-Content -Raw "package.json" | ConvertFrom-Json -AsHashtable
|
|
101
|
+
$packageJson.version = $emitterVersion
|
|
102
|
+
$packageJson | ConvertTo-Json -Depth 100 | Out-File -Path "package.json" -Encoding utf8 -NoNewline -Force
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Step 4: Create npm package
|
|
91
106
|
Write-Host "`n=== Creating npm package ===" -ForegroundColor Cyan
|
|
92
107
|
Invoke-LoggedCommand "npm pack"
|
|
93
108
|
Copy-Item "typespec-http-client-python-$emitterVersion.tgz" -Destination "$outputPath/packages"
|
|
94
109
|
|
|
95
|
-
# Step
|
|
110
|
+
# Step 5: Verify package can be installed
|
|
96
111
|
Write-Host "`n=== Verifying package installation ===" -ForegroundColor Cyan
|
|
97
112
|
Invoke-LoggedCommand "npm install typespec-http-client-python-$emitterVersion.tgz" -GroupOutput
|
|
98
113
|
|
|
@@ -677,7 +677,7 @@ export async function preprocess(flavor: string, generatedFolder: string): Promi
|
|
|
677
677
|
|
|
678
678
|
/**
|
|
679
679
|
* Resets the `tests/generated/{azure,unbranded}` baseline by sparse-checking-out
|
|
680
|
-
* `eng/tools/emitter/
|
|
680
|
+
* `eng/tools/azure-sdk-tools/emitter/generated` from the Azure/azure-sdk-for-python repo, then
|
|
681
681
|
* deleting a couple of fully-generated package folders so regeneration has to
|
|
682
682
|
* recreate them from scratch (smoke test of full-emit path).
|
|
683
683
|
*
|
|
@@ -686,8 +686,8 @@ export async function preprocess(flavor: string, generatedFolder: string): Promi
|
|
|
686
686
|
*/
|
|
687
687
|
export async function prepareBaselineOfGeneratedCode(generatedFolder: string): Promise<void> {
|
|
688
688
|
const repoUrl = "https://github.com/Azure/azure-sdk-for-python.git";
|
|
689
|
-
const branch = "
|
|
690
|
-
const sourceSubdir = "eng/tools/emitter/
|
|
689
|
+
const branch = "typespec-python-generated-tests";
|
|
690
|
+
const sourceSubdir = "eng/tools/azure-sdk-tools/emitter/generated";
|
|
691
691
|
const testsGeneratedDir = resolve(generatedFolder, "../tests/generated");
|
|
692
692
|
|
|
693
693
|
console.log(pc.cyan(`\n${"=".repeat(60)}`));
|
|
@@ -708,6 +708,7 @@ export async function prepareBaselineOfGeneratedCode(generatedFolder: string): P
|
|
|
708
708
|
execSync(cmd, { cwd: tempDir, stdio: ["ignore", "ignore", "inherit"] });
|
|
709
709
|
|
|
710
710
|
run(`git init`);
|
|
711
|
+
run(`git config core.longpaths true`);
|
|
711
712
|
run(`git remote add origin ${repoUrl}`);
|
|
712
713
|
run(`git config core.sparseCheckout true`);
|
|
713
714
|
run(`git sparse-checkout init --cone`);
|
|
@@ -732,16 +733,23 @@ export async function prepareBaselineOfGeneratedCode(generatedFolder: string): P
|
|
|
732
733
|
rmSync(tempDir, { recursive: true, force: true });
|
|
733
734
|
}
|
|
734
735
|
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
736
|
+
// Smoke test the full-emit path: delete a couple of fully-generated package
|
|
737
|
+
// folders and every README.md so regeneration has to recreate them.
|
|
738
|
+
const deleteIfExists = (path: string) => {
|
|
739
|
+
if (!existsSync(path)) return;
|
|
740
|
+
console.log(pc.dim(`Deleting ${path}`));
|
|
741
|
+
rmSync(path, { recursive: true, force: true });
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
deleteIfExists(join(testsGeneratedDir, "azure", "authentication-http-custom"));
|
|
745
|
+
deleteIfExists(join(testsGeneratedDir, "unbranded", "encode-array"));
|
|
746
|
+
|
|
747
|
+
if (existsSync(testsGeneratedDir)) {
|
|
748
|
+
const entries = await readdir(testsGeneratedDir, { recursive: true, withFileTypes: true });
|
|
749
|
+
for (const entry of entries) {
|
|
750
|
+
if (entry.isFile() && entry.name === "README.md") {
|
|
751
|
+
deleteIfExists(join(entry.parentPath, entry.name));
|
|
752
|
+
}
|
|
745
753
|
}
|
|
746
754
|
}
|
|
747
755
|
}
|
|
@@ -47,6 +47,15 @@ def _single_dir_pylint(mod):
|
|
|
47
47
|
]
|
|
48
48
|
if is_azure:
|
|
49
49
|
pylint_args.append("--load-plugins=pylint_guidelines_checker")
|
|
50
|
+
# Per-package suppressions for generated code where pylint's ignore-paths
|
|
51
|
+
# is not honored (pylint 4.x + recursive + plugin). Keep the scope as
|
|
52
|
+
# narrow as possible: only this package, only the offending check.
|
|
53
|
+
per_package_disables = {
|
|
54
|
+
"azure-client-generator-core-exact-name": ["client-incorrect-naming-convention"],
|
|
55
|
+
}
|
|
56
|
+
extra_disables = per_package_disables.get(mod.name)
|
|
57
|
+
if extra_disables:
|
|
58
|
+
pylint_args.append("--disable={}".format(",".join(extra_disables)))
|
|
50
59
|
pylint_args.append(str(inner_class.absolute()))
|
|
51
60
|
try:
|
|
52
61
|
check_call(pylint_args)
|
|
Binary file
|
|
Binary file
|
|
@@ -100,10 +100,20 @@ def install_packages(packages: list, venv_context=None, package_manager: str = N
|
|
|
100
100
|
|
|
101
101
|
try:
|
|
102
102
|
if cwd:
|
|
103
|
-
subprocess.
|
|
103
|
+
result = subprocess.run(install_cmd + packages, cwd=cwd, capture_output=True, text=True)
|
|
104
104
|
else:
|
|
105
|
-
subprocess.
|
|
106
|
-
|
|
105
|
+
result = subprocess.run(install_cmd + packages, capture_output=True, text=True)
|
|
106
|
+
if result.returncode != 0:
|
|
107
|
+
error_output = (result.stderr or "") + (result.stdout or "")
|
|
108
|
+
error_msg = f"Failed to install packages with {package_manager}: command exited with code {result.returncode}\n{error_output}"
|
|
109
|
+
if "401" in error_output or "Unauthorized" in error_output:
|
|
110
|
+
error_msg += (
|
|
111
|
+
"\n\nReceived a 401 Unauthorized error from the Azure feed. "
|
|
112
|
+
"A pip dependency may not yet be cached. To fix this, follow the authentication steps at: "
|
|
113
|
+
"https://github.com/Azure/azure-sdk-for-python/blob/main/CONTRIBUTING.md#authentication-for-upstream-pull-through"
|
|
114
|
+
)
|
|
115
|
+
raise RuntimeError(error_msg)
|
|
116
|
+
except OSError as e:
|
|
107
117
|
raise RuntimeError(f"Failed to install packages with {package_manager}: {e}")
|
|
108
118
|
|
|
109
119
|
|
|
@@ -91,11 +91,16 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
91
91
|
self.has_etag: bool = self.yaml_data.get("hasEtag", False)
|
|
92
92
|
self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
|
|
93
93
|
|
|
94
|
+
@property
|
|
95
|
+
def exact_name_params(self) -> set[str]:
|
|
96
|
+
"""Return the set of client names for parameters with isExactName."""
|
|
97
|
+
return {p.client_name for p in self.parameters.method if p.is_exact_name}
|
|
98
|
+
|
|
94
99
|
@property
|
|
95
100
|
def stream_value(self) -> Union[str, bool]:
|
|
96
101
|
return (
|
|
97
102
|
f'kwargs.pop("stream", {self.has_stream_response})'
|
|
98
|
-
if self.expose_stream_keyword and self.has_response_body
|
|
103
|
+
if self.expose_stream_keyword and self.has_response_body and "stream" not in self.exact_name_params
|
|
99
104
|
else self.has_stream_response
|
|
100
105
|
)
|
|
101
106
|
|
|
@@ -87,6 +87,7 @@ class _ParameterBase(BaseModel, abc.ABC): # pylint: disable=too-many-instance-a
|
|
|
87
87
|
self.default_to_unset_sentinel: bool = self.yaml_data.get("defaultToUnsetSentinel", False)
|
|
88
88
|
self.hide_in_method: bool = self.yaml_data.get("hideInMethod", False)
|
|
89
89
|
self.is_continuation_token: bool = bool(self.yaml_data.get("isContinuationToken"))
|
|
90
|
+
self.is_exact_name: bool = self.yaml_data.get("isExactName", False)
|
|
90
91
|
|
|
91
92
|
def get_declaration(self, value: Any = None) -> Any:
|
|
92
93
|
return self.type.get_declaration(value)
|
|
@@ -627,7 +627,8 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
627
627
|
return ""
|
|
628
628
|
|
|
629
629
|
def pop_kwargs_from_signature(self, builder: OperationType) -> list[str]:
|
|
630
|
-
|
|
630
|
+
exact_names = builder.exact_name_params
|
|
631
|
+
kwargs_to_pop = [k for k in builder.parameters.kwargs_to_pop if k.client_name not in exact_names]
|
|
631
632
|
kwargs = self.parameter_serializer.pop_kwargs_from_signature(
|
|
632
633
|
kwargs_to_pop,
|
|
633
634
|
check_kwarg_dict=True,
|
|
@@ -645,7 +646,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
645
646
|
body_parameter=builder.parameters.body_parameter if builder.parameters.has_body else None,
|
|
646
647
|
)
|
|
647
648
|
for p in builder.parameters.parameters:
|
|
648
|
-
if p.hide_in_operation_signature and not p.is_continuation_token:
|
|
649
|
+
if p.hide_in_operation_signature and not p.is_continuation_token and p.client_name not in exact_names:
|
|
649
650
|
kwargs.append(f'{p.client_name} = kwargs.pop("{p.client_name}", None)')
|
|
650
651
|
cls_annotation = builder.cls_type_annotation(
|
|
651
652
|
async_mode=self.async_mode, serialize_namespace=self.serialize_namespace
|
|
@@ -6,13 +6,73 @@
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
|
|
9
|
-
from ..models import ModelType, Property, ConstantType, EnumValue
|
|
9
|
+
from ..models import ModelType, Property, ConstantType, EnumValue, EnumType
|
|
10
10
|
from ..models.imports import FileImport, TypingSection, MsrestImportType, ImportType
|
|
11
|
+
from ..models.primitive_types import (
|
|
12
|
+
BooleanType,
|
|
13
|
+
IntegerType,
|
|
14
|
+
FloatType,
|
|
15
|
+
DecimalType,
|
|
16
|
+
StringType,
|
|
17
|
+
DatetimeType,
|
|
18
|
+
UnixTimeType,
|
|
19
|
+
TimeType,
|
|
20
|
+
DateType,
|
|
21
|
+
DurationType,
|
|
22
|
+
ByteArraySchema,
|
|
23
|
+
)
|
|
11
24
|
from .import_serializer import FileImportSerializer
|
|
12
25
|
from .base_serializer import BaseSerializer
|
|
13
26
|
from ..models.utils import NamespaceType
|
|
14
27
|
|
|
15
28
|
|
|
29
|
+
def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
|
|
30
|
+
"""Return the _xml_deser_* function name for a scalar XML property, or None."""
|
|
31
|
+
prop_type = prop.type
|
|
32
|
+
# Unwrap ConstantType to get the underlying value type
|
|
33
|
+
if isinstance(prop_type, ConstantType):
|
|
34
|
+
prop_type = prop_type.value_type
|
|
35
|
+
|
|
36
|
+
if isinstance(prop_type, StringType):
|
|
37
|
+
return "_xml_deser_str"
|
|
38
|
+
if isinstance(prop_type, IntegerType):
|
|
39
|
+
return "_xml_deser_int"
|
|
40
|
+
if isinstance(prop_type, FloatType):
|
|
41
|
+
return "_xml_deser_float"
|
|
42
|
+
if isinstance(prop_type, BooleanType):
|
|
43
|
+
return "_xml_deser_bool"
|
|
44
|
+
if isinstance(prop_type, DecimalType):
|
|
45
|
+
return "_xml_deser_decimal"
|
|
46
|
+
if isinstance(prop_type, DatetimeType):
|
|
47
|
+
if prop_type.encode == "rfc7231":
|
|
48
|
+
return "_xml_deser_datetime_rfc7231"
|
|
49
|
+
return "_xml_deser_datetime"
|
|
50
|
+
if isinstance(prop_type, UnixTimeType):
|
|
51
|
+
return "_xml_deser_datetime_unix_timestamp"
|
|
52
|
+
if isinstance(prop_type, TimeType):
|
|
53
|
+
return "_xml_deser_time"
|
|
54
|
+
if isinstance(prop_type, DateType):
|
|
55
|
+
return "_xml_deser_date"
|
|
56
|
+
if isinstance(prop_type, DurationType):
|
|
57
|
+
return "_xml_deser_duration"
|
|
58
|
+
if isinstance(prop_type, ByteArraySchema):
|
|
59
|
+
encode = getattr(prop_type, "encode", None) or getattr(prop, "encode", None)
|
|
60
|
+
if encode == "base64url":
|
|
61
|
+
return "_xml_deser_bytes_base64url"
|
|
62
|
+
return "_xml_deser_bytes"
|
|
63
|
+
if isinstance(prop_type, EnumType):
|
|
64
|
+
return f"enum:{prop_type.name}"
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_xml_deserializer_enum_type(prop: Property) -> Optional[EnumType]:
|
|
69
|
+
"""Return the EnumType for an XML enum property (unwrapping ConstantType), or None."""
|
|
70
|
+
prop_type = prop.type
|
|
71
|
+
if isinstance(prop_type, ConstantType):
|
|
72
|
+
prop_type = prop_type.value_type
|
|
73
|
+
return prop_type if isinstance(prop_type, EnumType) else None
|
|
74
|
+
|
|
75
|
+
|
|
16
76
|
def _documentation_string(prop: Property, description_keyword: str, docstring_type_keyword: str) -> list[str]:
|
|
17
77
|
retval: list[str] = []
|
|
18
78
|
sphinx_prefix = f":{description_keyword} {prop.client_name}:"
|
|
@@ -253,6 +313,9 @@ class DpgModelSerializer(_ModelSerializer):
|
|
|
253
313
|
TypingSection.REGULAR,
|
|
254
314
|
alias="_Model",
|
|
255
315
|
)
|
|
316
|
+
# Collect XML deserializer functions needed by models in this file
|
|
317
|
+
xml_deser_names: set[str] = set()
|
|
318
|
+
xml_deser_enums: dict[str, EnumType] = {}
|
|
256
319
|
for model in self.models:
|
|
257
320
|
if model.base == "json":
|
|
258
321
|
continue
|
|
@@ -271,6 +334,15 @@ class DpgModelSerializer(_ModelSerializer):
|
|
|
271
334
|
called_by_property=True,
|
|
272
335
|
)
|
|
273
336
|
)
|
|
337
|
+
# Track XML deserializer functions needed
|
|
338
|
+
if prop.xml_metadata:
|
|
339
|
+
deser_name = _get_xml_deserializer_name(prop)
|
|
340
|
+
if deser_name:
|
|
341
|
+
xml_deser_names.add(deser_name)
|
|
342
|
+
if deser_name and deser_name.startswith("enum:"):
|
|
343
|
+
enum_type = _get_xml_deserializer_enum_type(prop)
|
|
344
|
+
if enum_type is not None:
|
|
345
|
+
xml_deser_enums[enum_type.name] = enum_type
|
|
274
346
|
for parent in model.parents:
|
|
275
347
|
if parent.client_namespace != model.client_namespace:
|
|
276
348
|
file_import.add_submodule_import(
|
|
@@ -285,6 +357,38 @@ class DpgModelSerializer(_ModelSerializer):
|
|
|
285
357
|
file_import.add_submodule_import("typing", "overload", ImportType.STDLIB)
|
|
286
358
|
file_import.add_submodule_import("typing", "Mapping", ImportType.STDLIB)
|
|
287
359
|
file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
|
|
360
|
+
# Add imports for XML deserializer functions
|
|
361
|
+
has_enum_deser = False
|
|
362
|
+
for deser_name in sorted(xml_deser_names):
|
|
363
|
+
if deser_name.startswith("enum:"):
|
|
364
|
+
has_enum_deser = True
|
|
365
|
+
continue
|
|
366
|
+
file_import.add_submodule_import(
|
|
367
|
+
self.code_model.get_relative_import_path(self.serialize_namespace, module_name="_utils.model_base"),
|
|
368
|
+
deser_name,
|
|
369
|
+
ImportType.LOCAL,
|
|
370
|
+
)
|
|
371
|
+
if has_enum_deser:
|
|
372
|
+
file_import.add_import("functools", ImportType.STDLIB)
|
|
373
|
+
file_import.add_submodule_import(
|
|
374
|
+
self.code_model.get_relative_import_path(self.serialize_namespace, module_name="_utils.model_base"),
|
|
375
|
+
"_xml_deser_enum_or_str",
|
|
376
|
+
ImportType.LOCAL,
|
|
377
|
+
)
|
|
378
|
+
# Ensure each referenced enum class is imported at runtime (not just
|
|
379
|
+
# under TYPE_CHECKING), so functools.partial can bind the class at
|
|
380
|
+
# class-body evaluation time.
|
|
381
|
+
for enum_name, enum_type in xml_deser_enums.items():
|
|
382
|
+
file_import.add_submodule_import(
|
|
383
|
+
self.code_model.get_relative_import_path(
|
|
384
|
+
self.serialize_namespace,
|
|
385
|
+
self.code_model.get_imported_namespace_for_model(enum_type.client_namespace),
|
|
386
|
+
module_name=self.code_model.enums_filename,
|
|
387
|
+
),
|
|
388
|
+
enum_name,
|
|
389
|
+
ImportType.LOCAL,
|
|
390
|
+
TypingSection.REGULAR,
|
|
391
|
+
)
|
|
288
392
|
# if there is a property named `list` we have to make sure there's no conflict with the built-in `list`
|
|
289
393
|
if self.code_model.has_property_named_list:
|
|
290
394
|
file_import.define_mypy_type("List", "list")
|
|
@@ -337,6 +441,14 @@ class DpgModelSerializer(_ModelSerializer):
|
|
|
337
441
|
|
|
338
442
|
if prop.xml_metadata:
|
|
339
443
|
args.append(f"xml={prop.xml_metadata}")
|
|
444
|
+
# Add fast-path deserializer for scalar XML fields
|
|
445
|
+
deser_name = _get_xml_deserializer_name(prop)
|
|
446
|
+
if deser_name:
|
|
447
|
+
if deser_name.startswith("enum:"):
|
|
448
|
+
enum_name = deser_name[5:]
|
|
449
|
+
args.append(f"deserializer=functools.partial(_xml_deser_enum_or_str, {enum_name})")
|
|
450
|
+
else:
|
|
451
|
+
args.append(f"deserializer={deser_name}")
|
|
340
452
|
|
|
341
453
|
if prop.original_tsp_name:
|
|
342
454
|
args.append(f'original_tsp_name="{prop.original_tsp_name}"')
|