oed-cli 0.1.0__tar.gz → 0.1.2__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.
- {oed_cli-0.1.0/src/oed_cli.egg-info → oed_cli-0.1.2}/PKG-INFO +3 -3
- {oed_cli-0.1.0 → oed_cli-0.1.2}/README.md +2 -2
- {oed_cli-0.1.0 → oed_cli-0.1.2}/pyproject.toml +1 -1
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/__init__.py +1 -1
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/dynamic.py +72 -1
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/invoke.py +44 -2
- {oed_cli-0.1.0 → oed_cli-0.1.2/src/oed_cli.egg-info}/PKG-INFO +3 -3
- {oed_cli-0.1.0 → oed_cli-0.1.2}/tests/test_dynamic.py +143 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/LICENSE +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/setup.cfg +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/__main__.py +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/cli.py +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/discovery.py +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/errors.py +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/http.py +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/main.py +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli/py.typed +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli.egg-info/SOURCES.txt +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli.egg-info/dependency_links.txt +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli.egg-info/entry_points.txt +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli.egg-info/requires.txt +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/src/oed_cli.egg-info/top_level.txt +0 -0
- {oed_cli-0.1.0 → oed_cli-0.1.2}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oed-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: oed — openEuler Infra command line. Auto-discovered, AI-friendly.
|
|
5
5
|
Author: oed-cli contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -97,7 +97,7 @@ pip install -e .
|
|
|
97
97
|
Verify:
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
|
-
oed --version # → oed, version 0.2
|
|
100
|
+
oed --version # → oed, version 0.1.2
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
---
|
|
@@ -248,7 +248,7 @@ invocation. Drop it with `pip uninstall oed-cli` when you're done.
|
|
|
248
248
|
python -m pytest -q
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
41 tests cover v0.1 + v0.2 dispatch, the operation-help cheatsheet,
|
|
252
252
|
per-parameter flag coercion, the `API_`-prefix alias, the
|
|
253
253
|
`resolve_runtime_gateway` no-fallback semantics, and every exit code path.
|
|
254
254
|
They monkeypatch the discovery layer so no gateway access is needed.
|
|
@@ -63,7 +63,7 @@ pip install -e .
|
|
|
63
63
|
Verify:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
oed --version # → oed, version 0.2
|
|
66
|
+
oed --version # → oed, version 0.1.2
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
---
|
|
@@ -214,7 +214,7 @@ invocation. Drop it with `pip uninstall oed-cli` when you're done.
|
|
|
214
214
|
python -m pytest -q
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
41 tests cover v0.1 + v0.2 dispatch, the operation-help cheatsheet,
|
|
218
218
|
per-parameter flag coercion, the `API_`-prefix alias, the
|
|
219
219
|
`resolve_runtime_gateway` no-fallback semantics, and every exit code path.
|
|
220
220
|
They monkeypatch the discovery layer so no gateway access is needed.
|
|
@@ -149,6 +149,13 @@ class Operation:
|
|
|
149
149
|
description: str = ""
|
|
150
150
|
parameters: tuple[dict[str, Any], ...] = field(default_factory=tuple)
|
|
151
151
|
body_required: bool = False
|
|
152
|
+
# Raw ``requestBody`` block (so we don't lose the OpenAPI-level metadata)
|
|
153
|
+
# plus the inlined ``body_schema`` resolved against ``components/schemas``
|
|
154
|
+
# for the ``application/json`` content type — the latter is what agents
|
|
155
|
+
# consume to learn how to build a ``--json`` body. ``None`` when the op
|
|
156
|
+
# has no JSON body.
|
|
157
|
+
request_body: dict[str, Any] | None = None
|
|
158
|
+
body_schema: dict[str, Any] | None = None
|
|
152
159
|
operation_id: str = ""
|
|
153
160
|
# Resolved runtime base URL — read from ``ServiceMeta.base_url`` via
|
|
154
161
|
# :func:`resolve_runtime_gateway`. Empty by default so callers that
|
|
@@ -166,7 +173,12 @@ class Operation:
|
|
|
166
173
|
op: dict[str, Any],
|
|
167
174
|
backend: Backend,
|
|
168
175
|
base_url: str = "",
|
|
176
|
+
spec: dict[str, Any] | None = None,
|
|
169
177
|
) -> Operation:
|
|
178
|
+
request_body = op.get("requestBody") if isinstance(op.get("requestBody"), dict) else None
|
|
179
|
+
body_schema: dict[str, Any] | None = None
|
|
180
|
+
if request_body is not None and spec is not None:
|
|
181
|
+
body_schema = _resolve_json_body_schema(request_body, spec)
|
|
170
182
|
return cls(
|
|
171
183
|
service_name=service_name,
|
|
172
184
|
path=path,
|
|
@@ -176,6 +188,8 @@ class Operation:
|
|
|
176
188
|
description=op.get("description", ""),
|
|
177
189
|
parameters=tuple(op.get("parameters", []) or ()),
|
|
178
190
|
body_required=bool(op.get("requestBody", {}).get("required")),
|
|
191
|
+
request_body=request_body,
|
|
192
|
+
body_schema=body_schema,
|
|
179
193
|
operation_id=op.get("operationId") or f"{verb.upper()} {path}",
|
|
180
194
|
base_url=base_url,
|
|
181
195
|
)
|
|
@@ -224,6 +238,60 @@ def _parse_backend(op: dict[str, Any], *, path: str, verb: str) -> Backend:
|
|
|
224
238
|
)
|
|
225
239
|
|
|
226
240
|
|
|
241
|
+
# --------------------------------------------------------------------------- #
|
|
242
|
+
# $ref resolver for request-body schemas
|
|
243
|
+
# --------------------------------------------------------------------------- #
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _inline_refs(node: Any, *, spec: dict[str, Any], _seen: frozenset[str] = frozenset()) -> Any:
|
|
247
|
+
"""Recursively inline ``$ref: #/components/schemas/X`` in ``node``.
|
|
248
|
+
|
|
249
|
+
Only resolves refs the openEuler gateway actually emits (a single schema
|
|
250
|
+
bag under ``components.schemas``); external ``$ref``s and non-JSON-Reference
|
|
251
|
+
forms are left alone so the original spec stays recoverable for
|
|
252
|
+
diagnostics. Recursion depth is bounded by the ref-graph so a cyclic
|
|
253
|
+
schema can't blow the stack.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
if isinstance(node, dict):
|
|
257
|
+
if "$ref" in node and isinstance(node["$ref"], str):
|
|
258
|
+
ref = node["$ref"]
|
|
259
|
+
if ref.startswith("#/components/schemas/") and ref not in _seen:
|
|
260
|
+
key = ref.rsplit("/", 1)[-1]
|
|
261
|
+
schemas = (spec.get("components") or {}).get("schemas") or {}
|
|
262
|
+
target = schemas.get(key)
|
|
263
|
+
if isinstance(target, dict):
|
|
264
|
+
return _inline_refs(
|
|
265
|
+
target, spec=spec, _seen=_seen | {ref}
|
|
266
|
+
)
|
|
267
|
+
return node
|
|
268
|
+
return {k: _inline_refs(v, spec=spec, _seen=_seen) for k, v in node.items()}
|
|
269
|
+
if isinstance(node, list):
|
|
270
|
+
return [_inline_refs(v, spec=spec, _seen=_seen) for v in node]
|
|
271
|
+
return node
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _resolve_json_body_schema(
|
|
275
|
+
request_body: dict[str, Any], spec: dict[str, Any]
|
|
276
|
+
) -> dict[str, Any] | None:
|
|
277
|
+
"""Return the inlined JSON-body schema for a requestBody block.
|
|
278
|
+
|
|
279
|
+
Prefers the ``application/json`` content type; falls back to the first
|
|
280
|
+
declared content type when JSON isn't there. Returns ``None`` if the
|
|
281
|
+
block has no schema to resolve.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
content = request_body.get("content") or {}
|
|
285
|
+
media = content.get("application/json") or next(iter(content.values()), None)
|
|
286
|
+
if not isinstance(media, dict):
|
|
287
|
+
return None
|
|
288
|
+
schema = media.get("schema")
|
|
289
|
+
if not isinstance(schema, dict):
|
|
290
|
+
return None
|
|
291
|
+
resolved = _inline_refs(schema, spec=spec)
|
|
292
|
+
return resolved if isinstance(resolved, dict) else None
|
|
293
|
+
|
|
294
|
+
|
|
227
295
|
def collect_operations(
|
|
228
296
|
spec: dict[str, Any], service_name: str, *, base_url: str = ""
|
|
229
297
|
) -> list[Operation]:
|
|
@@ -232,7 +300,9 @@ def collect_operations(
|
|
|
232
300
|
``base_url`` is the resolved runtime URL (output of
|
|
233
301
|
:func:`resolve_runtime_gateway`); it is baked into every returned
|
|
234
302
|
:class:`Operation` so :mod:`oed_cli.invoke` does not need to know
|
|
235
|
-
which service produced the operation.
|
|
303
|
+
which service produced the operation. The ``spec`` is also passed
|
|
304
|
+
through so each operation's ``requestBody`` can be resolved against
|
|
305
|
+
``components/schemas``.
|
|
236
306
|
"""
|
|
237
307
|
|
|
238
308
|
out: list[Operation] = []
|
|
@@ -251,6 +321,7 @@ def collect_operations(
|
|
|
251
321
|
op=op,
|
|
252
322
|
backend=_parse_backend(op, path=path, verb=verb),
|
|
253
323
|
base_url=base_url,
|
|
324
|
+
spec=spec,
|
|
254
325
|
)
|
|
255
326
|
)
|
|
256
327
|
return out
|
|
@@ -34,6 +34,12 @@ def _fill_path(template: str, params: dict[str, Any]) -> tuple[str, list[str]]:
|
|
|
34
34
|
|
|
35
35
|
Returns the rendered path and the list of placeholder names that were
|
|
36
36
|
not provided, so the caller can raise a precise error.
|
|
37
|
+
|
|
38
|
+
Huawei APIG marks required path params with a trailing ``+`` in the
|
|
39
|
+
template (``/t/{id+}``); the ``+`` is a spec-side marker that never
|
|
40
|
+
appears in the real request URL or in the declared
|
|
41
|
+
``parameters[].name``. Strip it so the lookup matches what the
|
|
42
|
+
caller actually provided under ``--<flag>`` / ``--params``.
|
|
37
43
|
"""
|
|
38
44
|
|
|
39
45
|
missing: list[str] = []
|
|
@@ -48,13 +54,14 @@ def _fill_path(template: str, params: dict[str, Any]) -> tuple[str, list[str]]:
|
|
|
48
54
|
if name_end == -1:
|
|
49
55
|
parts.append(head + sep + chunk)
|
|
50
56
|
break
|
|
51
|
-
|
|
57
|
+
raw_name = chunk[:name_end]
|
|
58
|
+
name = raw_name.rstrip("+")
|
|
52
59
|
parts.append(head)
|
|
53
60
|
if name in params:
|
|
54
61
|
parts.append(str(params[name]))
|
|
55
62
|
else:
|
|
56
63
|
missing.append(name)
|
|
57
|
-
parts.append("{" +
|
|
64
|
+
parts.append("{" + raw_name + "}")
|
|
58
65
|
rest = chunk[name_end + 1 :]
|
|
59
66
|
return "".join(parts), missing
|
|
60
67
|
|
|
@@ -75,6 +82,36 @@ def _select_params(
|
|
|
75
82
|
return path_params, query_params, unused
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
def _body_field_summary(
|
|
86
|
+
schema: dict[str, Any] | None,
|
|
87
|
+
) -> list[dict[str, Any]]:
|
|
88
|
+
"""Project a resolved body schema down to ``[{name, type, required}]``.
|
|
89
|
+
|
|
90
|
+
Listing views use this so the per-operation summary stays compact
|
|
91
|
+
while still telling an agent which fields exist and which are
|
|
92
|
+
mandatory. The full schema is exposed separately under
|
|
93
|
+
``body_schema`` on ``oed <service> <op> --help``.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
if not isinstance(schema, dict):
|
|
97
|
+
return []
|
|
98
|
+
properties = schema.get("properties") or {}
|
|
99
|
+
required = set(schema.get("required") or [])
|
|
100
|
+
out: list[dict[str, Any]] = []
|
|
101
|
+
for name, prop in properties.items():
|
|
102
|
+
if not isinstance(prop, dict):
|
|
103
|
+
continue
|
|
104
|
+
entry: dict[str, Any] = {
|
|
105
|
+
"name": name,
|
|
106
|
+
"type": prop.get("type", "object"),
|
|
107
|
+
"required": name in required,
|
|
108
|
+
}
|
|
109
|
+
if prop.get("description"):
|
|
110
|
+
entry["description"] = prop["description"]
|
|
111
|
+
out.append(entry)
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
78
115
|
def _render_response(resp: httpx.Response) -> Any:
|
|
79
116
|
"""Parse the JSON body of ``resp``, returning a wrapped string for non-JSON."""
|
|
80
117
|
|
|
@@ -207,6 +244,8 @@ def describe_operation(op: Operation) -> dict[str, Any]:
|
|
|
207
244
|
"query_params": [p["name"] for p in op.query_params],
|
|
208
245
|
"body_required": op.body_required,
|
|
209
246
|
}
|
|
247
|
+
if op.body_required:
|
|
248
|
+
out["body_required_fields"] = _body_field_summary(op.body_schema)
|
|
210
249
|
if op.display_name != op.operation_id:
|
|
211
250
|
out["operation_id_raw"] = op.operation_id
|
|
212
251
|
return out
|
|
@@ -273,6 +312,9 @@ def describe_operation_help(op: Operation, service) -> dict[str, Any]:
|
|
|
273
312
|
}
|
|
274
313
|
if name != op.operation_id:
|
|
275
314
|
out["operation_id_raw"] = op.operation_id
|
|
315
|
+
if op.body_required:
|
|
316
|
+
out["body_required_fields"] = _body_field_summary(op.body_schema)
|
|
317
|
+
out["body_schema"] = op.body_schema
|
|
276
318
|
return out
|
|
277
319
|
|
|
278
320
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oed-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: oed — openEuler Infra command line. Auto-discovered, AI-friendly.
|
|
5
5
|
Author: oed-cli contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -97,7 +97,7 @@ pip install -e .
|
|
|
97
97
|
Verify:
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
|
-
oed --version # → oed, version 0.2
|
|
100
|
+
oed --version # → oed, version 0.1.2
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
---
|
|
@@ -248,7 +248,7 @@ invocation. Drop it with `pip uninstall oed-cli` when you're done.
|
|
|
248
248
|
python -m pytest -q
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
41 tests cover v0.1 + v0.2 dispatch, the operation-help cheatsheet,
|
|
252
252
|
per-parameter flag coercion, the `API_`-prefix alias, the
|
|
253
253
|
`resolve_runtime_gateway` no-fallback semantics, and every exit code path.
|
|
254
254
|
They monkeypatch the discovery layer so no gateway access is needed.
|
|
@@ -277,6 +277,106 @@ def test_collect_operations_without_backend_block():
|
|
|
277
277
|
assert op.base_url == "https://apig.osinfra.cn"
|
|
278
278
|
|
|
279
279
|
|
|
280
|
+
def test_request_body_schema_resolves_refs():
|
|
281
|
+
"""POST bodies that use ``$ref: #/components/schemas/X`` must surface
|
|
282
|
+
the resolved schema in ``Operation.body_schema`` so ``--help`` can
|
|
283
|
+
tell an agent how to construct the ``--json`` body."""
|
|
284
|
+
|
|
285
|
+
from oed_cli.dynamic import collect_operations
|
|
286
|
+
|
|
287
|
+
spec = {
|
|
288
|
+
"openapi": "3.0.1",
|
|
289
|
+
"components": {
|
|
290
|
+
"schemas": {
|
|
291
|
+
"SearchCondition": {
|
|
292
|
+
"type": "object",
|
|
293
|
+
"required": ["keyword", "lang"],
|
|
294
|
+
"properties": {
|
|
295
|
+
"keyword": {"type": "string", "description": "search kw"},
|
|
296
|
+
"lang": {"type": "string", "description": "zh|en"},
|
|
297
|
+
"page": {"type": "integer", "default": 1},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
"Pages": {"type": "object", "properties": {"size": {"type": "integer"}}},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
"paths": {
|
|
304
|
+
"/search/multitimodal": {
|
|
305
|
+
"post": {
|
|
306
|
+
"operationId": "multitimodalSearchDoc",
|
|
307
|
+
"requestBody": {
|
|
308
|
+
"required": True,
|
|
309
|
+
"content": {
|
|
310
|
+
"application/json": {
|
|
311
|
+
"schema": {"$ref": "#/components/schemas/SearchCondition"}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
"/cve/findAll": {
|
|
318
|
+
"post": {
|
|
319
|
+
"operationId": "findAllCVEDatabase",
|
|
320
|
+
"requestBody": {
|
|
321
|
+
"required": True,
|
|
322
|
+
"content": {
|
|
323
|
+
"application/json": {
|
|
324
|
+
"schema": {
|
|
325
|
+
"type": "object",
|
|
326
|
+
"properties": {
|
|
327
|
+
"keyword": {"type": "string"},
|
|
328
|
+
"pages": {"$ref": "#/components/schemas/Pages"},
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
ops = collect_operations(spec, "search", base_url="https://apig.osinfra.cn")
|
|
339
|
+
by_op = {op.operation_id: op for op in ops}
|
|
340
|
+
|
|
341
|
+
# Top-level $ref resolves to the schema body
|
|
342
|
+
sc = by_op["multitimodalSearchDoc"]
|
|
343
|
+
assert sc.body_required is True
|
|
344
|
+
assert sc.body_schema["required"] == ["keyword", "lang"]
|
|
345
|
+
assert set(sc.body_schema["properties"]) == {"keyword", "lang", "page"}
|
|
346
|
+
# The resolved schema no longer carries the $ref
|
|
347
|
+
assert "$ref" not in sc.body_schema
|
|
348
|
+
|
|
349
|
+
# Nested $ref (inside a property) inlines too
|
|
350
|
+
cve = by_op["findAllCVEDatabase"]
|
|
351
|
+
assert cve.body_schema["properties"]["pages"]["type"] == "object"
|
|
352
|
+
assert "size" in cve.body_schema["properties"]["pages"]["properties"]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_request_body_schema_cycles_do_not_blow_stack():
|
|
356
|
+
"""Cyclic ``$ref`` graphs (rare but possible) must terminate."""
|
|
357
|
+
|
|
358
|
+
from oed_cli.dynamic import _inline_refs
|
|
359
|
+
|
|
360
|
+
spec = {
|
|
361
|
+
"components": {
|
|
362
|
+
"schemas": {
|
|
363
|
+
"A": {
|
|
364
|
+
"type": "object",
|
|
365
|
+
"properties": {"next": {"$ref": "#/components/schemas/B"}},
|
|
366
|
+
},
|
|
367
|
+
"B": {
|
|
368
|
+
"type": "object",
|
|
369
|
+
"properties": {"next": {"$ref": "#/components/schemas/A"}},
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
out = _inline_refs({"$ref": "#/components/schemas/A"}, spec=spec)
|
|
375
|
+
# First level inlined; the cycle is broken at A's re-entry
|
|
376
|
+
assert out["type"] == "object"
|
|
377
|
+
assert "next" in out["properties"]
|
|
378
|
+
|
|
379
|
+
|
|
280
380
|
def test_parse_json_arg_rejects_garbage():
|
|
281
381
|
from oed_cli.dynamic import parse_json_arg
|
|
282
382
|
from oed_cli.errors import UserError
|
|
@@ -426,6 +526,49 @@ def test_call_operation_missing_path_param_raises_user_error(patched):
|
|
|
426
526
|
call_operation(op) # no id
|
|
427
527
|
|
|
428
528
|
|
|
529
|
+
def test_call_operation_strips_trailing_plus_in_path_template(patched):
|
|
530
|
+
"""Huawei APIG marks required path params with ``{name+}`` in the path
|
|
531
|
+
template (``/t/{id+}``). The declared ``parameters[].name`` is plain
|
|
532
|
+
``id`` — the ``+`` is a spec-side marker. ``oed forum getTopic --id 19308``
|
|
533
|
+
must therefore resolve the user's ``--id`` value into the rendered URL,
|
|
534
|
+
not complain that ``id+`` is missing."""
|
|
535
|
+
|
|
536
|
+
from oed_cli.dynamic import operations_table
|
|
537
|
+
from oed_cli.invoke import call_operation
|
|
538
|
+
|
|
539
|
+
spec = {
|
|
540
|
+
"openapi": "3.0.1",
|
|
541
|
+
"paths": {
|
|
542
|
+
"/t/{id+}": {
|
|
543
|
+
"get": {
|
|
544
|
+
"operationId": "getTopic",
|
|
545
|
+
"parameters": [
|
|
546
|
+
{"in": "path", "name": "id",
|
|
547
|
+
"required": True, "schema": {"type": "string"}},
|
|
548
|
+
],
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
table = operations_table(spec, "forum", base_url="https://apig.osinfra.cn")
|
|
554
|
+
op = table["getTopic"]
|
|
555
|
+
|
|
556
|
+
payload = call_operation(op, params={"id": "19308"})
|
|
557
|
+
assert patched["captured"]["url"].endswith("/t/19308")
|
|
558
|
+
assert payload["ok"] is True
|
|
559
|
+
assert payload["status"] == 200
|
|
560
|
+
|
|
561
|
+
# Missing-param error must use the clean name (what the user types),
|
|
562
|
+
# not the APIG-side ``id+`` form.
|
|
563
|
+
from oed_cli.errors import UserError
|
|
564
|
+
try:
|
|
565
|
+
call_operation(op)
|
|
566
|
+
except UserError as exc:
|
|
567
|
+
assert exc.to_dict()["message"] == "missing path params: ['id']"
|
|
568
|
+
else:
|
|
569
|
+
pytest.fail("expected UserError for missing path param")
|
|
570
|
+
|
|
571
|
+
|
|
429
572
|
# ---------- main.py dispatch ----------
|
|
430
573
|
|
|
431
574
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|