asana-api-cli 3.2.0__tar.gz → 3.3.0__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.
- {asana_api_cli-3.2.0/src/asana_api_cli.egg-info → asana_api_cli-3.3.0}/PKG-INFO +4 -2
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/README.md +3 -1
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/pyproject.toml +1 -1
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/cli.py +12 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/click_ext.py +18 -5
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/multibyte_filename.py +1 -1
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0/src/asana_api_cli.egg-info}/PKG-INFO +4 -2
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_cli_surface.py +1 -1
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_click_ext.py +87 -4
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_generate_python_snapshots.py +1 -1
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_sdk_boilerplate.py +22 -120
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/LICENSE +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/setup.cfg +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/__init__.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/codegen.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/formatter.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/redactor.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/session.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/structured_arg.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli/version.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli.egg-info/SOURCES.txt +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli.egg-info/dependency_links.txt +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli.egg-info/entry_points.txt +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli.egg-info/requires.txt +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/src/asana_api_cli.egg-info/top_level.txt +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_cli.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_cli_invocation.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_codegen.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_formatter.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_generate_python.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_multibyte_filename.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_py310_compat.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_redactor.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_session.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_structured_arg.py +0 -0
- {asana_api_cli-3.2.0 → asana_api_cli-3.3.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asana-api-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Command-line wrapper around the official Asana Python SDK
|
|
5
5
|
Author: Masanao Izumo
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,7 +135,9 @@ automatically.
|
|
|
135
135
|
|
|
136
136
|
The token can be issued from the
|
|
137
137
|
[Asana Developer Console](https://app.asana.com/0/developer-console).
|
|
138
|
-
No token is needed for `--help` or
|
|
138
|
+
No token is needed for `--help` or command-line parsing errors. (A `--query`
|
|
139
|
+
jq filter is validated against the response, so it surfaces errors only after
|
|
140
|
+
the API call — which does need a token.)
|
|
139
141
|
|
|
140
142
|
```bash
|
|
141
143
|
export ASANA_ACCESS_TOKEN="2/12345..."
|
|
@@ -107,7 +107,9 @@ automatically.
|
|
|
107
107
|
|
|
108
108
|
The token can be issued from the
|
|
109
109
|
[Asana Developer Console](https://app.asana.com/0/developer-console).
|
|
110
|
-
No token is needed for `--help` or
|
|
110
|
+
No token is needed for `--help` or command-line parsing errors. (A `--query`
|
|
111
|
+
jq filter is validated against the response, so it surfaces errors only after
|
|
112
|
+
the API call — which does need a token.)
|
|
111
113
|
|
|
112
114
|
```bash
|
|
113
115
|
export ASANA_ACCESS_TOKEN="2/12345..."
|
|
@@ -224,6 +224,7 @@ def _humanize_class_name(name: str) -> str:
|
|
|
224
224
|
# a release blocker.
|
|
225
225
|
_GROUP_DESCRIPTIONS: dict[str, str] = {
|
|
226
226
|
"AccessRequests": "Manage private-object access requests",
|
|
227
|
+
"Agents": "Read workspace AI agents (AI Teammates)",
|
|
227
228
|
"Allocations": "Manage user allocations across projects",
|
|
228
229
|
"Attachments": "Upload, list, and remove file attachments",
|
|
229
230
|
"AuditLogAPI": "Read domain audit log events",
|
|
@@ -238,6 +239,7 @@ _GROUP_DESCRIPTIONS: dict[str, str] = {
|
|
|
238
239
|
"Goals": "Manage organizational goals and metrics",
|
|
239
240
|
"Jobs": "Check status of async background jobs",
|
|
240
241
|
"Memberships": "Manage memberships across object types",
|
|
242
|
+
"OooEntries": "Manage out-of-office (OOO) entries",
|
|
241
243
|
"OrganizationExports": "Trigger and download org-wide exports",
|
|
242
244
|
"PortfolioMemberships": "Read who has access to portfolios",
|
|
243
245
|
"Portfolios": "Manage portfolios (project collections)",
|
|
@@ -1383,6 +1385,16 @@ def introspect_to_manifest() -> dict[str, Any]:
|
|
|
1383
1385
|
"positional": list(op.positional),
|
|
1384
1386
|
"has_opts": op.has_opts,
|
|
1385
1387
|
"paginatable": op.paginatable,
|
|
1388
|
+
# ``returns_iterator`` (array response → lazy iterator) and
|
|
1389
|
+
# ``does_upload`` (multipart file upload) pin the two runtime
|
|
1390
|
+
# classifiers that gate pagination materialization and the
|
|
1391
|
+
# ``--multibyte-filenames`` flag. Captured here so an SDK bump
|
|
1392
|
+
# that adds/removes such an endpoint surfaces in this fixture's
|
|
1393
|
+
# diff (the regen step), instead of in a hand-maintained set.
|
|
1394
|
+
# ``test_sdk_boilerplate.py`` still proves each classifier
|
|
1395
|
+
# matches the SDK source on the installed version.
|
|
1396
|
+
"returns_iterator": op.returns_iterator,
|
|
1397
|
+
"does_upload": op.does_upload,
|
|
1386
1398
|
"params": [
|
|
1387
1399
|
{
|
|
1388
1400
|
"name": p.name,
|
|
@@ -45,7 +45,7 @@ from asana_api_cli.structured_arg import (
|
|
|
45
45
|
# code path is naturally dead when the flag is hidden. We instantiate
|
|
46
46
|
# ``Configuration()`` because ``retry_strategy`` is an instance attribute
|
|
47
47
|
# set in ``__init__`` — checking ``hasattr`` on the class itself returns
|
|
48
|
-
# False even on 5.2.
|
|
48
|
+
# False even on 5.2.5.
|
|
49
49
|
_SDK_HAS_RETRY_STRATEGY: bool = hasattr(asana.Configuration(), "retry_strategy")
|
|
50
50
|
|
|
51
51
|
|
|
@@ -342,12 +342,22 @@ def _apply_global_to_runtime(name: str, value: Any) -> None:
|
|
|
342
342
|
option declarations and the dataclass fields are authored together, and the
|
|
343
343
|
inventory tests in ``test_click_ext.py`` (``TestGlobalOptionNamesInventory``)
|
|
344
344
|
pin the ``GLOBAL_OPTION_NAMES`` set against ``_Runtime``'s fields — so a
|
|
345
|
-
direct ``setattr`` suffices. Every option, ``access_token``
|
|
346
|
-
follows the same last-wins rule: the command-line value from the
|
|
347
|
-
level reached overwrites whatever an earlier level wrote. An
|
|
348
|
-
``--access-token`` therefore clears a value set earlier, and
|
|
345
|
+
direct ``setattr`` suffices. Every *scalar* option, ``access_token``
|
|
346
|
+
included, follows the same last-wins rule: the command-line value from the
|
|
347
|
+
deepest level reached overwrites whatever an earlier level wrote. An
|
|
348
|
+
explicit empty ``--access-token`` therefore clears a value set earlier, and
|
|
349
349
|
``AsanaSession.from_env`` then falls back to ``$ASANA_ACCESS_TOKEN``.
|
|
350
350
|
|
|
351
|
+
``default_headers`` is the one exception, because ``--set-default-header`` is
|
|
352
|
+
the only repeatable (accumulative) global: ``multiple=True`` already merges
|
|
353
|
+
every occurrence *at one level* into a single dict, so to honor "the later
|
|
354
|
+
occurrence wins when repeated" (``usage.md``) across levels too, values from
|
|
355
|
+
different levels are merged per-header rather than replaced wholesale. A
|
|
356
|
+
header set on the root group thus survives another ``--set-default-header``
|
|
357
|
+
given at the leaf, and a deeper level's value wins on a key collision. The
|
|
358
|
+
callback (:func:`structured_arg.default_header_callback`) yields ``None``
|
|
359
|
+
when the flag is absent, so only non-empty dicts ever reach this branch.
|
|
360
|
+
|
|
351
361
|
The caller, :func:`_consume_global_options`, only ever passes names drawn
|
|
352
362
|
from ``GLOBAL_OPTION_NAMES`` and only when the parameter source is
|
|
353
363
|
``ParameterSource.COMMANDLINE``. This is the single application path for
|
|
@@ -356,6 +366,9 @@ def _apply_global_to_runtime(name: str, value: Any) -> None:
|
|
|
356
366
|
tri-state toggles' (``verify_ssl`` / ``assert_hostname`` /
|
|
357
367
|
``return_page_iterator``) ``None`` default never reaches here.
|
|
358
368
|
"""
|
|
369
|
+
if name == "default_headers":
|
|
370
|
+
runtime.default_headers = {**(runtime.default_headers or {}), **value}
|
|
371
|
+
return
|
|
359
372
|
setattr(runtime, name, value)
|
|
360
373
|
|
|
361
374
|
|
|
@@ -16,7 +16,7 @@ from urllib3.fields import RequestField
|
|
|
16
16
|
class MultibyteFilenameSupport:
|
|
17
17
|
"""Make multipart uploads round-trip filenames with non-ASCII characters.
|
|
18
18
|
|
|
19
|
-
In ``python-asana`` 5.2.
|
|
19
|
+
In ``python-asana`` 5.2.5 (the latest version checked, and likely later
|
|
20
20
|
ones too), uploading a file whose name has characters outside ASCII
|
|
21
21
|
stores a garbled (mojibake) name on Asana: the SDK's multipart encoder
|
|
22
22
|
emits only ``filename="..."`` and omits the RFC 5987 ``filename*=``
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asana-api-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Command-line wrapper around the official Asana Python SDK
|
|
5
5
|
Author: Masanao Izumo
|
|
6
6
|
License-Expression: MIT
|
|
@@ -135,7 +135,9 @@ automatically.
|
|
|
135
135
|
|
|
136
136
|
The token can be issued from the
|
|
137
137
|
[Asana Developer Console](https://app.asana.com/0/developer-console).
|
|
138
|
-
No token is needed for `--help` or
|
|
138
|
+
No token is needed for `--help` or command-line parsing errors. (A `--query`
|
|
139
|
+
jq filter is validated against the response, so it surfaces errors only after
|
|
140
|
+
the API call — which does need a token.)
|
|
139
141
|
|
|
140
142
|
```bash
|
|
141
143
|
export ASANA_ACCESS_TOKEN="2/12345..."
|
|
@@ -34,7 +34,7 @@ FIXTURE = Path(__file__).parent / "fixtures" / "cli_surface.json"
|
|
|
34
34
|
# to diverge. Skip the snapshot tests when the SDK doesn't match rather
|
|
35
35
|
# than failing — the surface drift on older / newer SDKs is informational,
|
|
36
36
|
# not a regression to gate against.
|
|
37
|
-
_SNAPSHOT_ASANA_VERSION = "5.2.
|
|
37
|
+
_SNAPSHOT_ASANA_VERSION = "5.2.5"
|
|
38
38
|
_INSTALLED_ASANA_VERSION = version("asana")
|
|
39
39
|
|
|
40
40
|
pytestmark = pytest.mark.skipif(
|
|
@@ -68,6 +68,35 @@ def _build_cli() -> click.Group:
|
|
|
68
68
|
return root
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _build_cli_echoing_default_headers() -> click.Group:
|
|
72
|
+
"""A 3-level CLI (root → sub → leaf) like :func:`_build_cli` whose leaf echoes
|
|
73
|
+
the accumulated ``runtime.default_headers``.
|
|
74
|
+
|
|
75
|
+
Used to drive the *real* ``--set-default-header`` global through Click
|
|
76
|
+
parsing at more than one level, so ``_consume_global_options`` and
|
|
77
|
+
``_apply_global_to_runtime`` are exercised end-to-end (not called by hand)
|
|
78
|
+
and the test stays pinned to the option's actual dest.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@click.group(cls=GroupWithGlobalOptions)
|
|
82
|
+
def root() -> None:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@root.group("sub", cls=GroupWithGlobalOptions)
|
|
86
|
+
def sub() -> None:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@sub.command("act")
|
|
90
|
+
def act() -> None:
|
|
91
|
+
h = runtime.default_headers
|
|
92
|
+
if h is None:
|
|
93
|
+
click.echo("HEADERS=NONE")
|
|
94
|
+
else:
|
|
95
|
+
click.echo("HEADERS=" + "|".join(f"{k}={v}" for k, v in sorted(h.items())))
|
|
96
|
+
|
|
97
|
+
return root
|
|
98
|
+
|
|
99
|
+
|
|
71
100
|
class TestGlobalOptionsAtAnyLevel:
|
|
72
101
|
def test_debug_on_leaf_command(self) -> None:
|
|
73
102
|
result = make_runner().invoke(_build_cli(), ["tasks", "act", "--debug"])
|
|
@@ -225,10 +254,12 @@ class TestGlobalOptionNamesInventory:
|
|
|
225
254
|
|
|
226
255
|
# Reverse direction: every global option name must be an actual
|
|
227
256
|
# ``_Runtime`` field. ``_apply_global_to_runtime`` applies values with a
|
|
228
|
-
# bare ``setattr(runtime, name, value)
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
257
|
+
# bare ``setattr(runtime, name, value)`` (``default_headers`` is the one
|
|
258
|
+
# exception: it merges into the existing dict, but still writes that same
|
|
259
|
+
# field); ``_Runtime`` is neither frozen nor slotted, so a name without a
|
|
260
|
+
# matching field would silently create a stray attribute (a no-op option)
|
|
261
|
+
# rather than erroring. Pinning the 1:1 here is what lets that ``setattr``
|
|
262
|
+
# skip name validation.
|
|
232
263
|
runtime_fields = frozenset(f.name for f in dataclasses.fields(_Runtime))
|
|
233
264
|
for name in GLOBAL_OPTION_NAMES:
|
|
234
265
|
assert name in runtime_fields, (
|
|
@@ -327,6 +358,58 @@ class TestGlobalOptionsSingleSource:
|
|
|
327
358
|
)
|
|
328
359
|
|
|
329
360
|
|
|
361
|
+
class TestDefaultHeadersMergeAcrossLevels:
|
|
362
|
+
"""``--set-default-header`` is the only repeatable (accumulative) global, so
|
|
363
|
+
unlike the scalar globals — which are last-wins, see
|
|
364
|
+
``TestGlobalOptionsAtAnyLevel.test_leaf_overrides_root`` — headers given at
|
|
365
|
+
different points in the command path must *merge* per-header rather than the
|
|
366
|
+
inner level's dict replacing the outer one wholesale.
|
|
367
|
+
|
|
368
|
+
Driven through a real ``GroupWithGlobalOptions`` tree, so Click parsing,
|
|
369
|
+
``_consume_global_options`` and ``_apply_global_to_runtime`` all run end to
|
|
370
|
+
end and the assertions track the option's real dest."""
|
|
371
|
+
|
|
372
|
+
def test_headers_from_different_levels_merge(self) -> None:
|
|
373
|
+
result = make_runner().invoke(
|
|
374
|
+
_build_cli_echoing_default_headers(),
|
|
375
|
+
[
|
|
376
|
+
"--set-default-header",
|
|
377
|
+
"X-Root=root",
|
|
378
|
+
"--set-default-header",
|
|
379
|
+
"X-Shared=root",
|
|
380
|
+
"sub",
|
|
381
|
+
"act",
|
|
382
|
+
"--set-default-header",
|
|
383
|
+
"X-Leaf=leaf",
|
|
384
|
+
"--set-default-header",
|
|
385
|
+
"X-Shared=leaf",
|
|
386
|
+
],
|
|
387
|
+
)
|
|
388
|
+
assert result.exit_code == 0, full_output(result)
|
|
389
|
+
# X-Root (outer) survives; the X-Shared collision resolves to the deeper
|
|
390
|
+
# (leaf) value; keys are echoed sorted.
|
|
391
|
+
assert "HEADERS=X-Leaf=leaf|X-Root=root|X-Shared=leaf" in full_output(result)
|
|
392
|
+
|
|
393
|
+
def test_outer_header_not_clobbered_when_inner_omits_flag(self) -> None:
|
|
394
|
+
"""A header set at an outer level must persist when an inner level is
|
|
395
|
+
reached without its own ``--set-default-header`` (the inner level's
|
|
396
|
+
DEFAULT-sourced empty value is skipped, so nothing overwrites it)."""
|
|
397
|
+
result = make_runner().invoke(
|
|
398
|
+
_build_cli_echoing_default_headers(),
|
|
399
|
+
["--set-default-header", "X-Root=root", "sub", "act"],
|
|
400
|
+
)
|
|
401
|
+
assert result.exit_code == 0, full_output(result)
|
|
402
|
+
assert "HEADERS=X-Root=root" in full_output(result)
|
|
403
|
+
|
|
404
|
+
def test_no_header_flag_leaves_runtime_unset(self) -> None:
|
|
405
|
+
"""With the flag absent everywhere, the merge branch is never entered:
|
|
406
|
+
``runtime.default_headers`` stays at its ``None`` unset sentinel rather
|
|
407
|
+
than becoming a stray empty dict."""
|
|
408
|
+
result = make_runner().invoke(_build_cli_echoing_default_headers(), ["sub", "act"])
|
|
409
|
+
assert result.exit_code == 0, full_output(result)
|
|
410
|
+
assert "HEADERS=NONE" in full_output(result)
|
|
411
|
+
|
|
412
|
+
|
|
330
413
|
def _bash_complete(cmd: click.Command, args: list[str]) -> list[str]:
|
|
331
414
|
"""Drive click's bash completion machinery and return the suggested values."""
|
|
332
415
|
from click.shell_completion import ShellComplete
|
|
@@ -35,7 +35,7 @@ from asana_api_cli.cli import main
|
|
|
35
35
|
FIXTURES = Path(__file__).parent / "fixtures" / "generate_python"
|
|
36
36
|
_UPDATE = os.environ.get("UPDATE_GENERATE_SNAPSHOTS") == "1"
|
|
37
37
|
|
|
38
|
-
_SNAPSHOT_ASANA_VERSION = "5.2.
|
|
38
|
+
_SNAPSHOT_ASANA_VERSION = "5.2.5"
|
|
39
39
|
pytestmark = pytest.mark.skipif(
|
|
40
40
|
version("asana") != _SNAPSHOT_ASANA_VERSION,
|
|
41
41
|
reason=(
|
|
@@ -19,13 +19,27 @@ bump fails loudly and forces a conscious classification — a global flag
|
|
|
19
19
|
(Configuration) or a common per-command ``(kwargs: ...)`` option, with the
|
|
20
20
|
matching SDK-destination label (see ``docs/cli-sdk-mapping.md``).
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
``
|
|
28
|
-
|
|
22
|
+
Two further guards prove the CLI's runtime *classifiers* still agree with the
|
|
23
|
+
SDK source on the installed version — each derives both sides from the SDK, so
|
|
24
|
+
neither needs maintenance across a bump:
|
|
25
|
+
|
|
26
|
+
* ``test_upload_detection_matches_multipart_population`` — the
|
|
27
|
+
``--multibyte-filenames`` flag is exposed only on upload commands, detected by
|
|
28
|
+
a cheap proxy (an op declaring a ``file`` opt — see ``_Operation.does_upload``).
|
|
29
|
+
The test proves that proxy equals the true signal (a source scan for assignment
|
|
30
|
+
into ``local_var_files``).
|
|
31
|
+
* ``test_iterator_detection_matches_source_construction`` — array-response
|
|
32
|
+
endpoints return a lazy ``PageIterator`` / ``EventIterator`` the runtime
|
|
33
|
+
materializes with ``list(...)``; the runtime detects them by a cheap proxy
|
|
34
|
+
(``:return:`` type ending in ``Array`` — see ``_Operation.returns_iterator``).
|
|
35
|
+
The test proves that proxy equals the true signal (a source scan for the
|
|
36
|
+
iterator constructor).
|
|
37
|
+
|
|
38
|
+
The *which-commands* question — the exact set of upload / iterator endpoints —
|
|
39
|
+
is snapshotted per-command in ``tests/fixtures/cli_surface.json`` (the
|
|
40
|
+
``does_upload`` / ``returns_iterator`` fields), so an SDK bump that adds or
|
|
41
|
+
removes such an endpoint surfaces in that fixture's diff during the normal regen
|
|
42
|
+
step, with no hand-maintained set to update here.
|
|
29
43
|
"""
|
|
30
44
|
|
|
31
45
|
from __future__ import annotations
|
|
@@ -40,7 +54,7 @@ import asana
|
|
|
40
54
|
|
|
41
55
|
from asana_api_cli.cli import _enumerate_api_classes, _operations_for
|
|
42
56
|
|
|
43
|
-
# Identical across all ``*_with_http_info`` methods in python-asana 5.2.
|
|
57
|
+
# Identical across all ``*_with_http_info`` methods in python-asana 5.2.5.
|
|
44
58
|
EXPECTED_ALL_PARAMS: frozenset[str] = frozenset(
|
|
45
59
|
{
|
|
46
60
|
"async_req",
|
|
@@ -98,102 +112,6 @@ EXPECTED_CONFIGURATION_ATTRS: frozenset[str] = frozenset(
|
|
|
98
112
|
}
|
|
99
113
|
)
|
|
100
114
|
|
|
101
|
-
# The one python-asana method that performs a multipart file upload — i.e. it
|
|
102
|
-
# assigns into ``local_var_files`` (every other method has only the empty
|
|
103
|
-
# ``local_var_files = {}`` boilerplate). The CLI exposes ``--multibyte-filenames``
|
|
104
|
-
# only on upload commands, detected at runtime by a cheap proxy (an op declaring
|
|
105
|
-
# a ``file`` opt; see ``_Operation.does_upload``). This anchor plus the
|
|
106
|
-
# proxy-equality check below pin that proxy to the true multipart signal.
|
|
107
|
-
EXPECTED_MULTIPART_UPLOAD_METHODS: frozenset[tuple[str, str]] = frozenset(
|
|
108
|
-
{("AttachmentsApi", "create_attachment_for_object")}
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# The python-asana methods whose response is an array — they return a lazy
|
|
112
|
-
# ``PageIterator`` / ``EventIterator`` (built in the ``*_with_http_info`` source)
|
|
113
|
-
# under the default flags, which the CLI materializes with ``list(...)``. The
|
|
114
|
-
# runtime detects them via a cheap proxy (the ``:return:`` type ends in
|
|
115
|
-
# ``Array``; see ``_Operation.returns_iterator``). This anchor plus the
|
|
116
|
-
# proxy-equality check below pin that proxy to the true iterator-construction
|
|
117
|
-
# signal so an SDK bump that adds, removes, or reshapes an array endpoint fails
|
|
118
|
-
# loudly. (73 methods in python-asana 5.2.4.)
|
|
119
|
-
EXPECTED_ITERATOR_RETURNING_METHODS: frozenset[tuple[str, str]] = frozenset(
|
|
120
|
-
{
|
|
121
|
-
("AccessRequestsApi", "get_access_requests"),
|
|
122
|
-
("AllocationsApi", "get_allocations"),
|
|
123
|
-
("AttachmentsApi", "get_attachments_for_object"),
|
|
124
|
-
("AuditLogAPIApi", "get_audit_log_events"),
|
|
125
|
-
("BatchAPIApi", "create_batch_request"),
|
|
126
|
-
("BudgetsApi", "get_budgets"),
|
|
127
|
-
("CustomFieldSettingsApi", "get_custom_field_settings_for_goal"),
|
|
128
|
-
("CustomFieldSettingsApi", "get_custom_field_settings_for_portfolio"),
|
|
129
|
-
("CustomFieldSettingsApi", "get_custom_field_settings_for_project"),
|
|
130
|
-
("CustomFieldSettingsApi", "get_custom_field_settings_for_team"),
|
|
131
|
-
("CustomFieldsApi", "get_custom_fields_for_workspace"),
|
|
132
|
-
("CustomTypesApi", "get_custom_types"),
|
|
133
|
-
("EventsApi", "get_events"),
|
|
134
|
-
("GoalRelationshipsApi", "get_goal_relationships"),
|
|
135
|
-
("GoalsApi", "get_goals"),
|
|
136
|
-
("GoalsApi", "get_parent_goals_for_goal"),
|
|
137
|
-
("MembershipsApi", "get_memberships"),
|
|
138
|
-
("PortfolioMembershipsApi", "get_portfolio_memberships"),
|
|
139
|
-
("PortfolioMembershipsApi", "get_portfolio_memberships_for_portfolio"),
|
|
140
|
-
("PortfoliosApi", "get_items_for_portfolio"),
|
|
141
|
-
("PortfoliosApi", "get_portfolios"),
|
|
142
|
-
("ProjectMembershipsApi", "get_project_memberships_for_project"),
|
|
143
|
-
("ProjectPortfolioSettingsApi", "get_project_portfolio_settings_for_portfolio"),
|
|
144
|
-
("ProjectPortfolioSettingsApi", "get_project_portfolio_settings_for_project"),
|
|
145
|
-
("ProjectStatusesApi", "get_project_statuses_for_project"),
|
|
146
|
-
("ProjectTemplatesApi", "get_project_templates"),
|
|
147
|
-
("ProjectTemplatesApi", "get_project_templates_for_team"),
|
|
148
|
-
("ProjectsApi", "get_projects"),
|
|
149
|
-
("ProjectsApi", "get_projects_for_task"),
|
|
150
|
-
("ProjectsApi", "get_projects_for_team"),
|
|
151
|
-
("ProjectsApi", "get_projects_for_workspace"),
|
|
152
|
-
("ProjectsApi", "search_projects_for_workspace"),
|
|
153
|
-
("RatesApi", "get_rates"),
|
|
154
|
-
("ReactionsApi", "get_reactions_on_object"),
|
|
155
|
-
("RolesApi", "get_roles"),
|
|
156
|
-
("SectionsApi", "get_sections_for_project"),
|
|
157
|
-
("StatusUpdatesApi", "get_statuses_for_object"),
|
|
158
|
-
("StoriesApi", "get_stories_for_goal"),
|
|
159
|
-
("StoriesApi", "get_stories_for_task"),
|
|
160
|
-
("TagsApi", "get_tags"),
|
|
161
|
-
("TagsApi", "get_tags_for_task"),
|
|
162
|
-
("TagsApi", "get_tags_for_workspace"),
|
|
163
|
-
("TaskTemplatesApi", "get_task_templates"),
|
|
164
|
-
("TasksApi", "get_dependencies_for_task"),
|
|
165
|
-
("TasksApi", "get_dependents_for_task"),
|
|
166
|
-
("TasksApi", "get_subtasks_for_task"),
|
|
167
|
-
("TasksApi", "get_tasks"),
|
|
168
|
-
("TasksApi", "get_tasks_for_project"),
|
|
169
|
-
("TasksApi", "get_tasks_for_section"),
|
|
170
|
-
("TasksApi", "get_tasks_for_tag"),
|
|
171
|
-
("TasksApi", "get_tasks_for_user_task_list"),
|
|
172
|
-
("TasksApi", "search_tasks_for_workspace"),
|
|
173
|
-
("TeamMembershipsApi", "get_team_memberships"),
|
|
174
|
-
("TeamMembershipsApi", "get_team_memberships_for_team"),
|
|
175
|
-
("TeamMembershipsApi", "get_team_memberships_for_user"),
|
|
176
|
-
("TeamsApi", "get_teams_for_user"),
|
|
177
|
-
("TeamsApi", "get_teams_for_workspace"),
|
|
178
|
-
("TimePeriodsApi", "get_time_periods"),
|
|
179
|
-
("TimeTrackingCategoriesApi", "get_time_tracking_categories"),
|
|
180
|
-
("TimeTrackingCategoriesApi", "get_time_tracking_entries_for_time_tracking_category"),
|
|
181
|
-
("TimeTrackingEntriesApi", "get_time_tracking_entries"),
|
|
182
|
-
("TimeTrackingEntriesApi", "get_time_tracking_entries_for_task"),
|
|
183
|
-
("TimesheetApprovalStatusesApi", "get_timesheet_approval_statuses"),
|
|
184
|
-
("TypeaheadApi", "typeahead_for_workspace"),
|
|
185
|
-
("UsersApi", "get_favorites_for_user"),
|
|
186
|
-
("UsersApi", "get_users"),
|
|
187
|
-
("UsersApi", "get_users_for_team"),
|
|
188
|
-
("UsersApi", "get_users_for_workspace"),
|
|
189
|
-
("WebhooksApi", "get_webhooks"),
|
|
190
|
-
("WorkspaceMembershipsApi", "get_workspace_memberships_for_user"),
|
|
191
|
-
("WorkspaceMembershipsApi", "get_workspace_memberships_for_workspace"),
|
|
192
|
-
("WorkspacesApi", "get_workspace_events"),
|
|
193
|
-
("WorkspacesApi", "get_workspaces"),
|
|
194
|
-
}
|
|
195
|
-
)
|
|
196
|
-
|
|
197
115
|
|
|
198
116
|
def _all_params_for(func: Any) -> frozenset[str]:
|
|
199
117
|
"""Collect the ``all_params.append('x')`` string literals from a method body.
|
|
@@ -341,14 +259,6 @@ def test_no_unknown_configuration_settable_attrs() -> None:
|
|
|
341
259
|
def test_upload_detection_matches_multipart_population() -> None:
|
|
342
260
|
multipart = _methods_populating_local_var_files()
|
|
343
261
|
assert multipart, "no method populates local_var_files — SDK introspection broke"
|
|
344
|
-
assert multipart == EXPECTED_MULTIPART_UPLOAD_METHODS, (
|
|
345
|
-
"the set of multipart-upload methods drifted:\n"
|
|
346
|
-
f" added: {sorted(multipart - EXPECTED_MULTIPART_UPLOAD_METHODS)}\n"
|
|
347
|
-
f" removed: {sorted(EXPECTED_MULTIPART_UPLOAD_METHODS - multipart)}\n"
|
|
348
|
-
"A new/renamed upload endpoint must get the per-command "
|
|
349
|
-
"--multibyte-filenames flag (cli.py:_make_command, gated by "
|
|
350
|
-
"_Operation.does_upload). See docs/sdk-deviations.md."
|
|
351
|
-
)
|
|
352
262
|
via_proxy = _upload_ops_via_does_upload()
|
|
353
263
|
assert via_proxy == multipart, (
|
|
354
264
|
"_Operation.does_upload (the 'has a file opt' proxy) no longer matches "
|
|
@@ -365,14 +275,6 @@ def test_iterator_detection_matches_source_construction() -> None:
|
|
|
365
275
|
assert constructed, (
|
|
366
276
|
"no method constructs a PageIterator/EventIterator — SDK introspection broke"
|
|
367
277
|
)
|
|
368
|
-
assert constructed == EXPECTED_ITERATOR_RETURNING_METHODS, (
|
|
369
|
-
"the set of iterator-returning methods drifted:\n"
|
|
370
|
-
f" added: {sorted(constructed - EXPECTED_ITERATOR_RETURNING_METHODS)}\n"
|
|
371
|
-
f" removed: {sorted(EXPECTED_ITERATOR_RETURNING_METHODS - constructed)}\n"
|
|
372
|
-
"These are exactly the calls execute_call_plan materializes with "
|
|
373
|
-
"list(...); review the change and regenerate the set. See "
|
|
374
|
-
"_Operation.returns_iterator."
|
|
375
|
-
)
|
|
376
278
|
via_proxy = _iterator_ops_via_returns_iterator()
|
|
377
279
|
assert via_proxy == constructed, (
|
|
378
280
|
"_Operation.returns_iterator (the ':return: *Array' proxy) no longer "
|
|
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
|
|
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
|