tangle-cli 0.0.1a1__py3-none-any.whl
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.
- tangle_cli/__init__.py +19 -0
- tangle_cli/api_cli.py +787 -0
- tangle_cli/api_schema.py +633 -0
- tangle_cli/api_transport.py +461 -0
- tangle_cli/args_container.py +244 -0
- tangle_cli/artifacts.py +293 -0
- tangle_cli/artifacts_cli.py +108 -0
- tangle_cli/cli.py +57 -0
- tangle_cli/cli_helpers.py +116 -0
- tangle_cli/cli_options.py +52 -0
- tangle_cli/client.py +677 -0
- tangle_cli/component_from_func.py +1856 -0
- tangle_cli/component_generator.py +298 -0
- tangle_cli/component_inspector.py +494 -0
- tangle_cli/component_publisher.py +921 -0
- tangle_cli/components_cli.py +269 -0
- tangle_cli/dynamic_discovery_client.py +296 -0
- tangle_cli/generated_model_extensions.py +405 -0
- tangle_cli/generated_runtime.py +43 -0
- tangle_cli/handler.py +96 -0
- tangle_cli/hydration_trust.py +222 -0
- tangle_cli/logger.py +166 -0
- tangle_cli/models.py +407 -0
- tangle_cli/module_bundler.py +662 -0
- tangle_cli/openapi/__init__.py +0 -0
- tangle_cli/openapi/codegen.py +1090 -0
- tangle_cli/openapi/parser.py +77 -0
- tangle_cli/pipeline_dehydrator.py +720 -0
- tangle_cli/pipeline_hydrator.py +1785 -0
- tangle_cli/pipeline_run_annotations.py +41 -0
- tangle_cli/pipeline_run_details.py +203 -0
- tangle_cli/pipeline_run_manager.py +1994 -0
- tangle_cli/pipeline_run_search.py +712 -0
- tangle_cli/pipeline_runner.py +620 -0
- tangle_cli/pipeline_runs_cli.py +584 -0
- tangle_cli/pipelines.py +581 -0
- tangle_cli/pipelines_cli.py +271 -0
- tangle_cli/published_components_cli.py +373 -0
- tangle_cli/py.typed +0 -0
- tangle_cli/quickstart.py +110 -0
- tangle_cli/secrets.py +156 -0
- tangle_cli/secrets_cli.py +269 -0
- tangle_cli/utils.py +942 -0
- tangle_cli/version_manager.py +470 -0
- tangle_cli-0.0.1a1.dist-info/METADATA +561 -0
- tangle_cli-0.0.1a1.dist-info/RECORD +48 -0
- tangle_cli-0.0.1a1.dist-info/WHEEL +4 -0
- tangle_cli-0.0.1a1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from cyclopts import App, Parameter
|
|
6
|
+
|
|
7
|
+
from .cli_helpers import load_args_or_exit, optional_path
|
|
8
|
+
from .cli_options import ConfigOption, LogTypeOption
|
|
9
|
+
from .logger import logger_for_log_type
|
|
10
|
+
|
|
11
|
+
app = App(name="components", help="Work with Tangle component definitions.")
|
|
12
|
+
|
|
13
|
+
generate_app = App(name="generate", help="Generate component definition files.")
|
|
14
|
+
app.command(generate_app)
|
|
15
|
+
|
|
16
|
+
component_references_app = App(
|
|
17
|
+
name="component-references", help="Work with component reference metadata."
|
|
18
|
+
)
|
|
19
|
+
app.command(component_references_app)
|
|
20
|
+
|
|
21
|
+
annotations_app = App(name="annotations", help="Work with component annotations.")
|
|
22
|
+
app.command(annotations_app)
|
|
23
|
+
|
|
24
|
+
# region components
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command(name="validate")
|
|
28
|
+
def components_validate(component_path: str):
|
|
29
|
+
raise NotImplementedError()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command(name="set-container-image")
|
|
33
|
+
def components_set_container_image(component_path: str):
|
|
34
|
+
raise NotImplementedError()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# endregion
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# region components/annotations
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _missing_required_args(command_name: str, provided: dict[str, object]) -> None:
|
|
44
|
+
"""Print help for truly empty commands, but error on partial invocations."""
|
|
45
|
+
|
|
46
|
+
if all(value is None for value in provided.values()):
|
|
47
|
+
annotations_app.help_print([command_name])
|
|
48
|
+
raise SystemExit(0)
|
|
49
|
+
|
|
50
|
+
missing = [name for name, value in provided.items() if value is None]
|
|
51
|
+
print(f"Missing required argument(s): {', '.join(missing)}", file=sys.stderr)
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@annotations_app.command(name="set")
|
|
56
|
+
def components_annotations_set(
|
|
57
|
+
component_path: str | None = None,
|
|
58
|
+
key: str | None = None,
|
|
59
|
+
value: str | None = None,
|
|
60
|
+
output_component_path: str | None = None,
|
|
61
|
+
):
|
|
62
|
+
"""Sets annotation value in component file."""
|
|
63
|
+
if component_path is None or key is None or value is None:
|
|
64
|
+
_missing_required_args(
|
|
65
|
+
"set",
|
|
66
|
+
{"component_path": component_path, "key": key, "value": value},
|
|
67
|
+
)
|
|
68
|
+
raise NotImplementedError()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@annotations_app.command(name="get")
|
|
72
|
+
def components_annotations_get(
|
|
73
|
+
component_path: str | None = None, keys: list[str] | None = None
|
|
74
|
+
):
|
|
75
|
+
"""Gets annotation values from component file."""
|
|
76
|
+
if component_path is None or keys is None:
|
|
77
|
+
_missing_required_args("get", {"component_path": component_path, "keys": keys})
|
|
78
|
+
raise NotImplementedError()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# endregion
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# region components/generate
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@generate_app.command(name="from-template", show=False)
|
|
88
|
+
def components_generate_from_template(
|
|
89
|
+
template_name: str,
|
|
90
|
+
output_component_path: pathlib.Path,
|
|
91
|
+
):
|
|
92
|
+
raise NotImplementedError()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _components_generate_from_python_impl(
|
|
96
|
+
*,
|
|
97
|
+
python_file: pathlib.Path | None = None,
|
|
98
|
+
output: pathlib.Path | None = None,
|
|
99
|
+
name: str | None = None,
|
|
100
|
+
function_name: str | None = None,
|
|
101
|
+
image: str | None = None,
|
|
102
|
+
dependencies_from: pathlib.Path | None = None,
|
|
103
|
+
strip_code: bool | None = None,
|
|
104
|
+
use_legacy_naming: bool | None = None,
|
|
105
|
+
mode: str | None = None,
|
|
106
|
+
resolve_root: pathlib.Path | None = None,
|
|
107
|
+
config: str | None = None,
|
|
108
|
+
log_type: str = "console",
|
|
109
|
+
) -> None:
|
|
110
|
+
all_args = load_args_or_exit(
|
|
111
|
+
config,
|
|
112
|
+
python_file=("python_file", python_file, None, False, True, optional_path),
|
|
113
|
+
output=(output, None, optional_path),
|
|
114
|
+
name=(name, None),
|
|
115
|
+
function_name=("function", function_name, None, False),
|
|
116
|
+
image=(image, None),
|
|
117
|
+
dependencies_from=(dependencies_from, None, optional_path),
|
|
118
|
+
strip_code=(strip_code, None),
|
|
119
|
+
use_legacy_naming=(use_legacy_naming, None),
|
|
120
|
+
mode=(mode, None),
|
|
121
|
+
resolve_root=(resolve_root, None, optional_path),
|
|
122
|
+
log_type=(log_type, "console"),
|
|
123
|
+
)
|
|
124
|
+
for args in all_args:
|
|
125
|
+
logger, finalize_logs = logger_for_log_type(args.log_type)
|
|
126
|
+
from .component_generator import ComponentGenerator
|
|
127
|
+
|
|
128
|
+
generator = ComponentGenerator(logger=logger, verbose=True)
|
|
129
|
+
selected_mode = args.mode or "inline"
|
|
130
|
+
if selected_mode not in {"inline", "bundle"}:
|
|
131
|
+
raise SystemExit("--mode must be 'inline' or 'bundle'")
|
|
132
|
+
python_path = pathlib.Path(args.python_file)
|
|
133
|
+
output_path = generator.determine_output_path(
|
|
134
|
+
python_path,
|
|
135
|
+
args.output,
|
|
136
|
+
output_is_dir=False,
|
|
137
|
+
use_legacy_naming=bool(args.use_legacy_naming),
|
|
138
|
+
)
|
|
139
|
+
try:
|
|
140
|
+
success = generator.regenerate_yaml(
|
|
141
|
+
python_file=python_path,
|
|
142
|
+
output_path=output_path,
|
|
143
|
+
function_name=args.function_name,
|
|
144
|
+
custom_name=args.name,
|
|
145
|
+
image=args.image,
|
|
146
|
+
dependencies_from=args.dependencies_from,
|
|
147
|
+
strip_code=bool(args.strip_code),
|
|
148
|
+
mode=selected_mode,
|
|
149
|
+
resolve_root=args.resolve_root,
|
|
150
|
+
)
|
|
151
|
+
if not success:
|
|
152
|
+
raise SystemExit(1)
|
|
153
|
+
finally:
|
|
154
|
+
finalize_logs()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@generate_app.command(name="from-python")
|
|
158
|
+
def components_generate_from_python(
|
|
159
|
+
python_file: pathlib.Path | None = None,
|
|
160
|
+
*,
|
|
161
|
+
output: pathlib.Path | None = None,
|
|
162
|
+
name: str | None = None,
|
|
163
|
+
function_name: Annotated[
|
|
164
|
+
str | None,
|
|
165
|
+
Parameter(name="--function", alias="-f", help="Function name to extract."),
|
|
166
|
+
] = None,
|
|
167
|
+
image: str | None = None,
|
|
168
|
+
dependencies_from: pathlib.Path | None = None,
|
|
169
|
+
strip_code: bool | None = None,
|
|
170
|
+
use_legacy_naming: bool | None = None,
|
|
171
|
+
mode: str | None = None,
|
|
172
|
+
resolve_root: pathlib.Path | None = None,
|
|
173
|
+
config: ConfigOption = None,
|
|
174
|
+
log_type: LogTypeOption = "console",
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Generate a component YAML file from a local Python function."""
|
|
177
|
+
|
|
178
|
+
_components_generate_from_python_impl(
|
|
179
|
+
python_file=python_file,
|
|
180
|
+
output=output,
|
|
181
|
+
name=name,
|
|
182
|
+
function_name=function_name,
|
|
183
|
+
image=image,
|
|
184
|
+
dependencies_from=dependencies_from,
|
|
185
|
+
strip_code=strip_code,
|
|
186
|
+
use_legacy_naming=use_legacy_naming,
|
|
187
|
+
mode=mode,
|
|
188
|
+
resolve_root=resolve_root,
|
|
189
|
+
config=config,
|
|
190
|
+
log_type=log_type,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@generate_app.command(name="from-python-function")
|
|
195
|
+
def components_generate_from_python_function(
|
|
196
|
+
python_file: pathlib.Path | None = None,
|
|
197
|
+
*,
|
|
198
|
+
output: pathlib.Path | None = None,
|
|
199
|
+
name: str | None = None,
|
|
200
|
+
function_name: Annotated[
|
|
201
|
+
str | None,
|
|
202
|
+
Parameter(name="--function", alias="-f", help="Function name to extract."),
|
|
203
|
+
] = None,
|
|
204
|
+
image: str | None = None,
|
|
205
|
+
dependencies_from: pathlib.Path | None = None,
|
|
206
|
+
strip_code: bool | None = None,
|
|
207
|
+
use_legacy_naming: bool | None = None,
|
|
208
|
+
mode: str | None = None,
|
|
209
|
+
resolve_root: pathlib.Path | None = None,
|
|
210
|
+
config: ConfigOption = None,
|
|
211
|
+
log_type: LogTypeOption = "console",
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Compatibility alias for `generate from-python`."""
|
|
214
|
+
|
|
215
|
+
_components_generate_from_python_impl(
|
|
216
|
+
python_file=python_file,
|
|
217
|
+
output=output,
|
|
218
|
+
name=name,
|
|
219
|
+
function_name=function_name,
|
|
220
|
+
image=image,
|
|
221
|
+
dependencies_from=dependencies_from,
|
|
222
|
+
strip_code=strip_code,
|
|
223
|
+
use_legacy_naming=use_legacy_naming,
|
|
224
|
+
mode=mode,
|
|
225
|
+
resolve_root=resolve_root,
|
|
226
|
+
config=config,
|
|
227
|
+
log_type=log_type,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# endregion
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@app.command(name="bump-version")
|
|
235
|
+
def components_bump_version(
|
|
236
|
+
yaml_file: pathlib.Path | None = None,
|
|
237
|
+
*,
|
|
238
|
+
set_version: str | None = None,
|
|
239
|
+
update_timestamp: bool | None = None,
|
|
240
|
+
config: ConfigOption = None,
|
|
241
|
+
log_type: LogTypeOption = "console",
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Bump version metadata in a component YAML file."""
|
|
244
|
+
|
|
245
|
+
all_args = load_args_or_exit(
|
|
246
|
+
config,
|
|
247
|
+
yaml_file=("yaml_file", yaml_file, None, False, True, optional_path),
|
|
248
|
+
set_version=(set_version, None),
|
|
249
|
+
update_timestamp=(update_timestamp, None),
|
|
250
|
+
log_type=(log_type, "console"),
|
|
251
|
+
)
|
|
252
|
+
result: dict[str, Any] = {}
|
|
253
|
+
from .version_manager import bump_version
|
|
254
|
+
|
|
255
|
+
for args in all_args:
|
|
256
|
+
logger, finalize_logs = logger_for_log_type(args.log_type)
|
|
257
|
+
try:
|
|
258
|
+
result = bump_version(
|
|
259
|
+
args.yaml_file,
|
|
260
|
+
set_version=args.set_version,
|
|
261
|
+
update_timestamp=bool(args.update_timestamp),
|
|
262
|
+
logger=logger,
|
|
263
|
+
)
|
|
264
|
+
if result.get("status") != "success":
|
|
265
|
+
raise SystemExit(1)
|
|
266
|
+
finally:
|
|
267
|
+
finalize_logs()
|
|
268
|
+
if result:
|
|
269
|
+
print(result)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Programmatic dynamic-discovery client for Tangle backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .api_schema import (
|
|
11
|
+
OperationCommand,
|
|
12
|
+
fetch_schema,
|
|
13
|
+
load_cached_schema,
|
|
14
|
+
operation_aliases,
|
|
15
|
+
operation_map,
|
|
16
|
+
refresh_schema,
|
|
17
|
+
resolve_operation,
|
|
18
|
+
)
|
|
19
|
+
from .api_transport import (
|
|
20
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
21
|
+
_normalize_base_url,
|
|
22
|
+
default_base_url,
|
|
23
|
+
request_operation,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TangleDynamicDiscoveryClient:
|
|
28
|
+
"""Dynamic-discovery client generated from a Tangle OpenAPI schema.
|
|
29
|
+
|
|
30
|
+
The client intentionally reuses the same schema cache, operation naming,
|
|
31
|
+
parameter mapping, URL construction, and auth/header handling as
|
|
32
|
+
``tangle api ...``. No network or cache work happens at import time; choose
|
|
33
|
+
one of the ``from_*`` constructors to provide or load a schema.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
schema: dict[str, Any],
|
|
39
|
+
*,
|
|
40
|
+
base_url: str | None = None,
|
|
41
|
+
headers: dict[str, str] | None = None,
|
|
42
|
+
token: str | None = None,
|
|
43
|
+
auth_header: str | None = None,
|
|
44
|
+
header: list[str] | str | None = None,
|
|
45
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.schema = schema
|
|
48
|
+
self.base_url = _normalize_base_url(base_url or default_base_url())
|
|
49
|
+
self.headers = dict(headers or {})
|
|
50
|
+
self.token = token
|
|
51
|
+
self.auth_header = auth_header
|
|
52
|
+
self.header = _header_list(header)
|
|
53
|
+
self.timeout = timeout
|
|
54
|
+
self._operations = operation_map(schema)
|
|
55
|
+
self._aliases = self._build_alias_map(self._operations)
|
|
56
|
+
self._groups = self._build_groups(self._operations)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_schema(
|
|
60
|
+
cls,
|
|
61
|
+
schema: dict[str, Any],
|
|
62
|
+
*,
|
|
63
|
+
base_url: str | None = None,
|
|
64
|
+
headers: dict[str, str] | None = None,
|
|
65
|
+
token: str | None = None,
|
|
66
|
+
auth_header: str | None = None,
|
|
67
|
+
header: list[str] | str | None = None,
|
|
68
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
69
|
+
) -> TangleDynamicDiscoveryClient:
|
|
70
|
+
"""Create a client from an already loaded OpenAPI schema."""
|
|
71
|
+
|
|
72
|
+
return cls(
|
|
73
|
+
schema,
|
|
74
|
+
base_url=base_url,
|
|
75
|
+
headers=headers,
|
|
76
|
+
token=token,
|
|
77
|
+
auth_header=auth_header,
|
|
78
|
+
header=header,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_cache(
|
|
84
|
+
cls,
|
|
85
|
+
base_url: str | None = None,
|
|
86
|
+
*,
|
|
87
|
+
headers: dict[str, str] | None = None,
|
|
88
|
+
token: str | None = None,
|
|
89
|
+
auth_header: str | None = None,
|
|
90
|
+
header: list[str] | str | None = None,
|
|
91
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
92
|
+
) -> TangleDynamicDiscoveryClient:
|
|
93
|
+
"""Create a client from the local schema cache without network access."""
|
|
94
|
+
|
|
95
|
+
normalized_base_url = _normalize_base_url(base_url or default_base_url())
|
|
96
|
+
schema = load_cached_schema(normalized_base_url)
|
|
97
|
+
if schema is None:
|
|
98
|
+
raise FileNotFoundError(
|
|
99
|
+
f"No cached OpenAPI schema for {normalized_base_url}; "
|
|
100
|
+
"call TangleDynamicDiscoveryClient.from_cache_or_refresh(...) or run `tangle api refresh`."
|
|
101
|
+
)
|
|
102
|
+
return cls.from_schema(
|
|
103
|
+
schema,
|
|
104
|
+
base_url=normalized_base_url,
|
|
105
|
+
headers=headers,
|
|
106
|
+
token=token,
|
|
107
|
+
auth_header=auth_header,
|
|
108
|
+
header=header,
|
|
109
|
+
timeout=timeout,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_url(
|
|
114
|
+
cls,
|
|
115
|
+
base_url: str | None = None,
|
|
116
|
+
*,
|
|
117
|
+
headers: dict[str, str] | None = None,
|
|
118
|
+
token: str | None = None,
|
|
119
|
+
auth_header: str | None = None,
|
|
120
|
+
header: list[str] | str | None = None,
|
|
121
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
122
|
+
) -> TangleDynamicDiscoveryClient:
|
|
123
|
+
"""Fetch ``/openapi.json`` and create a client without writing the cache."""
|
|
124
|
+
|
|
125
|
+
normalized_base_url = _normalize_base_url(base_url or default_base_url())
|
|
126
|
+
schema = fetch_schema(
|
|
127
|
+
normalized_base_url,
|
|
128
|
+
token=token,
|
|
129
|
+
header=header,
|
|
130
|
+
auth_header=auth_header,
|
|
131
|
+
headers=headers,
|
|
132
|
+
)
|
|
133
|
+
return cls.from_schema(
|
|
134
|
+
schema,
|
|
135
|
+
base_url=normalized_base_url,
|
|
136
|
+
headers=headers,
|
|
137
|
+
token=token,
|
|
138
|
+
auth_header=auth_header,
|
|
139
|
+
header=header,
|
|
140
|
+
timeout=timeout,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_cache_or_refresh(
|
|
145
|
+
cls,
|
|
146
|
+
base_url: str | None = None,
|
|
147
|
+
*,
|
|
148
|
+
headers: dict[str, str] | None = None,
|
|
149
|
+
token: str | None = None,
|
|
150
|
+
auth_header: str | None = None,
|
|
151
|
+
header: list[str] | str | None = None,
|
|
152
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
153
|
+
) -> TangleDynamicDiscoveryClient:
|
|
154
|
+
"""Create a client from cache, fetching and caching the schema on miss."""
|
|
155
|
+
|
|
156
|
+
normalized_base_url = _normalize_base_url(base_url or default_base_url())
|
|
157
|
+
schema = load_cached_schema(normalized_base_url)
|
|
158
|
+
if schema is None:
|
|
159
|
+
schema, _ = refresh_schema(
|
|
160
|
+
normalized_base_url,
|
|
161
|
+
token=token,
|
|
162
|
+
header=header,
|
|
163
|
+
auth_header=auth_header,
|
|
164
|
+
headers=headers,
|
|
165
|
+
)
|
|
166
|
+
return cls.from_schema(
|
|
167
|
+
schema,
|
|
168
|
+
base_url=normalized_base_url,
|
|
169
|
+
headers=headers,
|
|
170
|
+
token=token,
|
|
171
|
+
auth_header=auth_header,
|
|
172
|
+
header=header,
|
|
173
|
+
timeout=timeout,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def operations(self) -> tuple[str, ...]:
|
|
178
|
+
"""Canonical operation names exposed by this schema."""
|
|
179
|
+
|
|
180
|
+
return tuple(sorted(self._operations))
|
|
181
|
+
|
|
182
|
+
def request(self, operation_name: str, **params: Any) -> httpx.Response:
|
|
183
|
+
"""Perform an operation and return the raw ``httpx.Response``.
|
|
184
|
+
|
|
185
|
+
Operation parameters are passed as keyword arguments. Per-call overrides
|
|
186
|
+
for ``base_url``, ``token``, ``auth_header``, ``header``, ``headers``,
|
|
187
|
+
``body``, and ``timeout`` are also supported.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
operation = self._resolve(operation_name)
|
|
191
|
+
base_url = params.pop("base_url", self.base_url)
|
|
192
|
+
token = params.pop("token", self.token)
|
|
193
|
+
auth_header = params.pop("auth_header", self.auth_header)
|
|
194
|
+
header_override = params.pop("header", None)
|
|
195
|
+
header = self.header + _header_list(header_override)
|
|
196
|
+
headers_override = params.pop("headers", None)
|
|
197
|
+
headers = {**self.headers, **dict(headers_override or {})}
|
|
198
|
+
body = params.pop("body", None)
|
|
199
|
+
timeout = params.pop("timeout", self.timeout)
|
|
200
|
+
return request_operation(
|
|
201
|
+
operation,
|
|
202
|
+
params,
|
|
203
|
+
base_url=base_url,
|
|
204
|
+
token=token,
|
|
205
|
+
auth_header=auth_header,
|
|
206
|
+
header_entries=header,
|
|
207
|
+
headers=headers,
|
|
208
|
+
body=body,
|
|
209
|
+
timeout=timeout,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def call(self, operation_name: str, **params: Any) -> Any:
|
|
213
|
+
"""Perform an operation and decode the response body.
|
|
214
|
+
|
|
215
|
+
JSON responses return decoded JSON; text responses return ``str``;
|
|
216
|
+
non-text responses return ``bytes``; empty responses return ``None``.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
return decode_response(self.request(operation_name, **params))
|
|
220
|
+
|
|
221
|
+
def __getattr__(self, name: str) -> _OperationGroup:
|
|
222
|
+
canonical_group = self._groups.get(name) or self._groups.get(name.replace("_", "-"))
|
|
223
|
+
if canonical_group is None:
|
|
224
|
+
raise AttributeError(name)
|
|
225
|
+
return _OperationGroup(self, canonical_group)
|
|
226
|
+
|
|
227
|
+
def _resolve(self, operation_name: str) -> OperationCommand:
|
|
228
|
+
return self._aliases.get(operation_name) or resolve_operation(self._operations, operation_name)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _build_alias_map(
|
|
232
|
+
operations: dict[str, OperationCommand]
|
|
233
|
+
) -> dict[str, OperationCommand]:
|
|
234
|
+
aliases: dict[str, OperationCommand] = {}
|
|
235
|
+
for name, operation in operations.items():
|
|
236
|
+
for alias in operation_aliases(name):
|
|
237
|
+
aliases.setdefault(alias, operation)
|
|
238
|
+
return aliases
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _build_groups(
|
|
242
|
+
operations: dict[str, OperationCommand]
|
|
243
|
+
) -> dict[str, str]:
|
|
244
|
+
groups: dict[str, str] = {}
|
|
245
|
+
for operation in operations.values():
|
|
246
|
+
groups.setdefault(operation.group_name, operation.group_name)
|
|
247
|
+
groups.setdefault(operation.group_name.replace("-", "_"), operation.group_name)
|
|
248
|
+
return groups
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class _OperationGroup:
|
|
252
|
+
"""Dynamic operation namespace returned by ``client.<resource>``."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, client: TangleDynamicDiscoveryClient, group_name: str) -> None:
|
|
255
|
+
self._client = client
|
|
256
|
+
self._group_name = group_name
|
|
257
|
+
|
|
258
|
+
def call(self, command_name: str, **params: Any) -> Any:
|
|
259
|
+
return self._client.call(f"{self._group_name}.{command_name}", **params)
|
|
260
|
+
|
|
261
|
+
def request(self, command_name: str, **params: Any) -> httpx.Response:
|
|
262
|
+
return self._client.request(f"{self._group_name}.{command_name}", **params)
|
|
263
|
+
|
|
264
|
+
def __getattr__(self, name: str) -> Callable[..., Any]:
|
|
265
|
+
operation_name = f"{self._group_name}.{name}"
|
|
266
|
+
try:
|
|
267
|
+
self._client._resolve(operation_name)
|
|
268
|
+
except KeyError as exc:
|
|
269
|
+
raise AttributeError(name) from exc
|
|
270
|
+
|
|
271
|
+
def operation(**params: Any) -> Any:
|
|
272
|
+
return self._client.call(operation_name, **params)
|
|
273
|
+
|
|
274
|
+
operation.__name__ = name
|
|
275
|
+
return operation
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _header_list(header: list[str] | str | None) -> list[str]:
|
|
279
|
+
if header is None:
|
|
280
|
+
return []
|
|
281
|
+
if isinstance(header, str):
|
|
282
|
+
return [header]
|
|
283
|
+
return list(header)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def decode_response(response: httpx.Response) -> Any:
|
|
287
|
+
"""Decode an ``httpx.Response`` into JSON, text, bytes, or ``None``."""
|
|
288
|
+
|
|
289
|
+
if not response.content:
|
|
290
|
+
return None
|
|
291
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
292
|
+
if "json" in content_type:
|
|
293
|
+
return response.json()
|
|
294
|
+
if content_type.startswith("text/") or "charset=" in content_type:
|
|
295
|
+
return response.text
|
|
296
|
+
return response.content
|