asana-api-cli 3.1.1__tar.gz → 3.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.
- {asana_api_cli-3.1.1/src/asana_api_cli.egg-info → asana_api_cli-3.1.2}/PKG-INFO +1 -1
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/pyproject.toml +1 -1
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/cli.py +133 -346
- asana_api_cli-3.1.2/src/asana_api_cli/click_ext.py +570 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/formatter.py +78 -72
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/redactor.py +2 -2
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/session.py +71 -76
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2/src/asana_api_cli.egg-info}/PKG-INFO +1 -1
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_cli.py +54 -29
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_cli_invocation.py +63 -29
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_click_ext.py +62 -78
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_formatter.py +26 -23
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_redactor.py +2 -1
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_session.py +96 -56
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_structured_arg.py +15 -3
- asana_api_cli-3.1.1/src/asana_api_cli/click_ext.py +0 -540
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/LICENSE +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/README.md +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/setup.cfg +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/__init__.py +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/structured_arg.py +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli/version.py +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli.egg-info/SOURCES.txt +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli.egg-info/dependency_links.txt +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli.egg-info/entry_points.txt +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli.egg-info/requires.txt +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/src/asana_api_cli.egg-info/top_level.txt +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_cli_surface.py +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_py310_compat.py +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_sdk_boilerplate.py +0 -0
- {asana_api_cli-3.1.1 → asana_api_cli-3.1.2}/tests/test_version.py +0 -0
|
@@ -54,31 +54,27 @@ import asana
|
|
|
54
54
|
import click
|
|
55
55
|
|
|
56
56
|
from asana_api_cli.click_ext import (
|
|
57
|
-
_SDK_HAS_RETRY_STRATEGY,
|
|
58
57
|
CommandWithGlobalOptions,
|
|
59
58
|
GroupWithGlobalOptions,
|
|
60
59
|
LazyGroup,
|
|
61
|
-
_make_global_option_params,
|
|
62
60
|
)
|
|
63
|
-
from asana_api_cli.formatter import formatted, formatter_flag_names
|
|
61
|
+
from asana_api_cli.formatter import formatted, formatter_flag_names, make_formatter_options
|
|
64
62
|
from asana_api_cli.session import (
|
|
65
63
|
AsanaSession,
|
|
66
|
-
|
|
64
|
+
MultibyteFilenameSupport,
|
|
67
65
|
)
|
|
68
66
|
from asana_api_cli.structured_arg import (
|
|
69
|
-
RETRY_FIELD_SCHEMA,
|
|
70
67
|
click_callback,
|
|
71
|
-
default_header_callback,
|
|
72
68
|
)
|
|
73
69
|
from asana_api_cli.version import version_string
|
|
74
70
|
|
|
75
71
|
# ---------------------------------------------------------------------------
|
|
76
72
|
# Input resolution
|
|
77
73
|
#
|
|
78
|
-
# Turn a raw CLI option value into the argument the SDK call receives,
|
|
79
|
-
#
|
|
80
|
-
# helpers — no SDK client / session involved — called only
|
|
81
|
-
# callback below.
|
|
74
|
+
# Turn a raw CLI option value into the argument the SDK call receives, raising
|
|
75
|
+
# ``click.BadParameter`` (exit code 2, user-input error) on bad input. These are
|
|
76
|
+
# pure invocation-layer helpers — no SDK client / session involved — called only
|
|
77
|
+
# from the command callback below.
|
|
82
78
|
# ---------------------------------------------------------------------------
|
|
83
79
|
|
|
84
80
|
DEFAULT_WORKSPACE_ENV = "ASANA_DEFAULT_WORKSPACE"
|
|
@@ -101,32 +97,30 @@ def resolve_body(value: str) -> JsonValue:
|
|
|
101
97
|
# stdin is reconfigured to UTF-8 at startup (see ``main``), so
|
|
102
98
|
# non-UTF-8 input from a pipe surfaces here instead of being
|
|
103
99
|
# silently misdecoded with the locale code page.
|
|
104
|
-
click.
|
|
105
|
-
|
|
100
|
+
raise click.BadParameter(
|
|
101
|
+
f"stdin is not valid UTF-8: {exc}", param_hint="--body"
|
|
102
|
+
) from exc
|
|
106
103
|
elif value.startswith("@"):
|
|
107
104
|
path = Path(value[1:])
|
|
108
105
|
try:
|
|
109
106
|
raw = path.read_text(encoding="utf-8")
|
|
110
|
-
except FileNotFoundError:
|
|
111
|
-
click.
|
|
112
|
-
sys.exit(2)
|
|
107
|
+
except FileNotFoundError as exc:
|
|
108
|
+
raise click.BadParameter(f"file not found: {path}", param_hint="--body") from exc
|
|
113
109
|
except UnicodeDecodeError as exc:
|
|
114
|
-
click.
|
|
115
|
-
f"
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
sys.exit(2)
|
|
110
|
+
raise click.BadParameter(
|
|
111
|
+
f"file {path} is not valid UTF-8: {exc}", param_hint="--body"
|
|
112
|
+
) from exc
|
|
119
113
|
except OSError as exc:
|
|
120
|
-
click.
|
|
121
|
-
|
|
114
|
+
raise click.BadParameter(
|
|
115
|
+
f"cannot read file {path}: {exc}", param_hint="--body"
|
|
116
|
+
) from exc
|
|
122
117
|
else:
|
|
123
118
|
raw = value
|
|
124
119
|
|
|
125
120
|
try:
|
|
126
121
|
return json.loads(raw)
|
|
127
122
|
except json.JSONDecodeError as exc:
|
|
128
|
-
click.
|
|
129
|
-
sys.exit(2)
|
|
123
|
+
raise click.BadParameter(f"invalid JSON: {exc}", param_hint="--body") from exc
|
|
130
124
|
|
|
131
125
|
|
|
132
126
|
def resolve_workspace(
|
|
@@ -144,7 +138,7 @@ def resolve_workspace(
|
|
|
144
138
|
alongside other scope parameters (e.g. ``--project`` on ``get-tasks``)
|
|
145
139
|
that the Asana API accepts in place of workspace.
|
|
146
140
|
|
|
147
|
-
If *required* is True and no value is found,
|
|
141
|
+
If *required* is True and no value is found, raises ``click.BadParameter``.
|
|
148
142
|
"""
|
|
149
143
|
if explicit is not None:
|
|
150
144
|
return explicit
|
|
@@ -152,11 +146,9 @@ def resolve_workspace(
|
|
|
152
146
|
ws = os.environ.get(DEFAULT_WORKSPACE_ENV)
|
|
153
147
|
if ws:
|
|
154
148
|
return ws
|
|
155
|
-
click.
|
|
156
|
-
f"
|
|
157
|
-
err=True,
|
|
149
|
+
raise click.BadParameter(
|
|
150
|
+
f"required (or set {DEFAULT_WORKSPACE_ENV})", param_hint="--workspace"
|
|
158
151
|
)
|
|
159
|
-
sys.exit(2)
|
|
160
152
|
return None
|
|
161
153
|
|
|
162
154
|
|
|
@@ -285,9 +277,11 @@ def _group_short_help(class_name: str) -> str:
|
|
|
285
277
|
|
|
286
278
|
|
|
287
279
|
# Hand-written help text for endpoint-local options whose SDK ``:param:``
|
|
288
|
-
# docstring is empty. The
|
|
289
|
-
# is the
|
|
290
|
-
#
|
|
280
|
+
# docstring is empty. The key is ``(group_class, method_name, param_name)``
|
|
281
|
+
# where ``group_class`` is the ``*Api`` class name with the ``Api`` suffix
|
|
282
|
+
# stripped (e.g. ``"Attachments"``, not ``"AttachmentsApi"`` — the lookup uses
|
|
283
|
+
# ``api_cls.__name__[:-3]``); bare param names (``file``, ``parent``, ``name``)
|
|
284
|
+
# would otherwise collide across endpoints. Sourced from Asana's developer
|
|
291
285
|
# reference. Lookup is conditional — only used when the SDK provides no
|
|
292
286
|
# description — so an SDK that later fills in a description silently wins
|
|
293
287
|
# over the override (which is fine, the SDK text is authoritative).
|
|
@@ -357,14 +351,14 @@ def _escape_help(text: str) -> str:
|
|
|
357
351
|
# (asana-api: extension) no SDK counterpart (CLI-only)
|
|
358
352
|
#
|
|
359
353
|
# Configuration globals and the two ApiClient-instance globals (--user-agent /
|
|
360
|
-
# --set-default-header) carry the literal
|
|
361
|
-
# ``
|
|
362
|
-
#
|
|
363
|
-
# flags (``--output`` / ``--query`` /
|
|
364
|
-
# ``--
|
|
365
|
-
# helper builds every
|
|
366
|
-
# ``_make_command`` derives at runtime: ``args`` / ``opts`` for path /
|
|
367
|
-
# docstring params, ``kwargs`` for the common per-call kwargs, and the
|
|
354
|
+
# --set-default-header) carry the literal in their single declaration in
|
|
355
|
+
# ``click_ext.py:_global_option_sections`` (the one source every command's
|
|
356
|
+
# globals are built from, so the label is identical at the root and at any
|
|
357
|
+
# subcommand); the CLI-only formatter flags (``--output`` / ``--query`` /
|
|
358
|
+
# ``--csv-bom`` and the error-path twins ``--exception-output`` /
|
|
359
|
+
# ``--exception-query``) live in ``formatter.py:make_formatter_options``. This helper builds every
|
|
360
|
+
# label ``_make_command`` derives at runtime: ``args`` / ``opts`` for path /
|
|
361
|
+
# body / docstring params, ``kwargs`` for the common per-call kwargs, and the
|
|
368
362
|
# extension marker on the deprecated aliases.
|
|
369
363
|
def _sdk_dest(category: str, name: str = "") -> str:
|
|
370
364
|
if category == "args":
|
|
@@ -440,7 +434,7 @@ def _parse_params(doc: str) -> dict[str, _DocParam]:
|
|
|
440
434
|
p.description = p.description.replace("(required)", "").strip()
|
|
441
435
|
|
|
442
436
|
# `_PARAM_RE` already drops the SDK's `:param async_req bool` line (no
|
|
443
|
-
# colon after the
|
|
437
|
+
# colon after the name, so the regex never matches). Kept as a guard in
|
|
444
438
|
# case the SDK docstring format changes to the colon form.
|
|
445
439
|
params.pop("async_req", None)
|
|
446
440
|
return params
|
|
@@ -543,11 +537,13 @@ class _Operation:
|
|
|
543
537
|
|
|
544
538
|
@property
|
|
545
539
|
def workspace_required(self) -> bool:
|
|
546
|
-
"""Workspace is required
|
|
540
|
+
"""Workspace is required when it is a path positional.
|
|
547
541
|
|
|
548
542
|
An ``opts`` workspace is always optional — no python-asana method marks
|
|
549
|
-
a query param ``(required)
|
|
550
|
-
|
|
543
|
+
a query param ``(required)`` — so the return's second branch
|
|
544
|
+
(``wo.required``) is a defensive guard that does not fire on today's SDK.
|
|
545
|
+
Drives the ``ASANA_DEFAULT_WORKSPACE`` env-var fallback: auto-fill only
|
|
546
|
+
when required.
|
|
551
547
|
"""
|
|
552
548
|
wo = self.workspace_opt
|
|
553
549
|
return self.workspace_positional is not None or (wo is not None and wo.required)
|
|
@@ -665,36 +661,16 @@ def _make_per_call_kwarg_options() -> list[click.Option]:
|
|
|
665
661
|
|
|
666
662
|
@functools.cache
|
|
667
663
|
def _static_reserved_flags() -> frozenset[str]:
|
|
668
|
-
"""
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
``
|
|
672
|
-
|
|
673
|
-
renames / additions automatically. Per-command conditional flags
|
|
674
|
-
(deprecated aliases, ``--multibyte-filenames``) are added in
|
|
675
|
-
:func:`_reserved_flags`.
|
|
676
|
-
|
|
677
|
-
``--help`` is added by click at parse time (never in ``cmd.params``) and
|
|
678
|
-
``--version`` is root-only, so neither is discoverable by scanning a leaf's
|
|
679
|
-
params — they are listed explicitly so a future SDK ``help`` / ``version``
|
|
680
|
-
param is still pushed to ``--sdk-help`` / ``--sdk-version``.
|
|
664
|
+
"""All asana-api own flags (no SDK counterpart). An SDK arg/opt whose
|
|
665
|
+
derived flag collides with one is exposed as ``--sdk-<name>``
|
|
666
|
+
(:func:`_decls`) so the built-in keeps its bare name. ``--help`` /
|
|
667
|
+
``--version`` are listed explicitly because neither appears in a leaf's
|
|
668
|
+
params.
|
|
681
669
|
"""
|
|
682
670
|
flags: set[str] = {"--help", "--version"}
|
|
683
671
|
flags |= formatter_flag_names()
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
flags.update(p.opts)
|
|
687
|
-
flags.update(getattr(p, "secondary_opts", []))
|
|
688
|
-
return frozenset(flags)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
def _reserved_flags(op: _Operation) -> frozenset[str]:
|
|
692
|
-
"""Built-in flags this command occupies (static set + per-command extras)."""
|
|
693
|
-
flags = set(_static_reserved_flags())
|
|
694
|
-
if op.paginatable:
|
|
695
|
-
flags |= {"--all-items", "--page-size", "--max-items"}
|
|
696
|
-
if op.does_upload:
|
|
697
|
-
flags.add("--multibyte-filenames")
|
|
672
|
+
# pagination aliases + upload toggle; keep in sync with _make_command
|
|
673
|
+
flags |= {"--all-items", "--page-size", "--max-items", "--multibyte-filenames"}
|
|
698
674
|
return frozenset(flags)
|
|
699
675
|
|
|
700
676
|
|
|
@@ -704,7 +680,7 @@ def _decls(flag: str, dest: str, reserved: frozenset[str]) -> list[str]:
|
|
|
704
680
|
If ``flag`` collides with a built-in CLI flag (in ``reserved``), the SDK
|
|
705
681
|
param yields: it is exposed as ``--sdk-<name>`` with an *explicit* ``dest``
|
|
706
682
|
equal to the SDK param name, so the call path (which pops by param name) is
|
|
707
|
-
unchanged and the ``(opts/
|
|
683
|
+
unchanged and the ``(opts/args: <name>)`` help label still shows the real
|
|
708
684
|
name. Otherwise the bare ``[flag]`` is used (dest auto-derives to ``dest``).
|
|
709
685
|
"""
|
|
710
686
|
if flag in reserved:
|
|
@@ -809,6 +785,54 @@ def _plain_opt_option(
|
|
|
809
785
|
return click.Option(_decls(flag, p.name, reserved), **kw)
|
|
810
786
|
|
|
811
787
|
|
|
788
|
+
def _apply_deprecated_aliases(kwargs: dict[str, Any], item_limit: int | None) -> int | None:
|
|
789
|
+
"""Resolve the deprecated pagination aliases and return the effective item
|
|
790
|
+
limit. Pops ``all_items`` / ``page_size`` / ``max_items`` from ``kwargs``
|
|
791
|
+
(absent on non-paginatable commands), warns on stderr, and folds
|
|
792
|
+
``page_size`` into ``kwargs["limit"]`` / ``max_items`` into the item limit,
|
|
793
|
+
with the canonical ``--limit`` / ``--item-limit`` winning when both are given.
|
|
794
|
+
"""
|
|
795
|
+
all_items = kwargs.pop("all_items", False)
|
|
796
|
+
page_size = kwargs.pop("page_size", None)
|
|
797
|
+
max_items = kwargs.pop("max_items", None)
|
|
798
|
+
|
|
799
|
+
if all_items:
|
|
800
|
+
click.echo(
|
|
801
|
+
"warning: --all-items is deprecated; walking every page is "
|
|
802
|
+
"now the default (will be removed in a future release)",
|
|
803
|
+
err=True,
|
|
804
|
+
)
|
|
805
|
+
if page_size is not None:
|
|
806
|
+
click.echo(
|
|
807
|
+
"warning: --page-size is deprecated; use --limit instead "
|
|
808
|
+
"(will be removed in a future release)",
|
|
809
|
+
err=True,
|
|
810
|
+
)
|
|
811
|
+
# Canonical --limit wins when both are given.
|
|
812
|
+
if kwargs.get("limit") is None:
|
|
813
|
+
kwargs["limit"] = page_size
|
|
814
|
+
if max_items is not None:
|
|
815
|
+
click.echo(
|
|
816
|
+
"warning: --max-items is deprecated; use --item-limit "
|
|
817
|
+
"instead (will be removed in a future release)",
|
|
818
|
+
err=True,
|
|
819
|
+
)
|
|
820
|
+
# Canonical --item-limit wins when both are given.
|
|
821
|
+
if item_limit is None:
|
|
822
|
+
item_limit = max_items
|
|
823
|
+
return item_limit
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _multibyte_filenames_callback(ctx: click.Context, param: click.Parameter, value: bool) -> None:
|
|
827
|
+
"""``--multibyte-filenames`` callback: install the RFC 5987 multipart patch
|
|
828
|
+
for this command and uninstall it at context teardown. ``with_resource``
|
|
829
|
+
enters the context manager now (install) and exits it when the command
|
|
830
|
+
finishes (uninstall), so the patch is scoped to this one invocation.
|
|
831
|
+
"""
|
|
832
|
+
if value:
|
|
833
|
+
ctx.with_resource(MultibyteFilenameSupport())
|
|
834
|
+
|
|
835
|
+
|
|
812
836
|
def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
813
837
|
"""Build a :class:`CommandWithGlobalOptions` for a single SDK method.
|
|
814
838
|
|
|
@@ -822,6 +846,9 @@ def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
|
822
846
|
so there is no required/optional split to order by.
|
|
823
847
|
3. **Boilerplate per-call kwargs** (the SDK ``all_params``), plus the
|
|
824
848
|
upload / deprecation extensions where they apply.
|
|
849
|
+
4. **Output-formatting options** (``--output`` / ``--query`` / ``--csv-bom``
|
|
850
|
+
/ ``--exception-output`` / ``--exception-query``) from
|
|
851
|
+
``make_formatter_options``, appended last.
|
|
825
852
|
"""
|
|
826
853
|
# If the SDK method has no ``opts`` parameter, docstring-derived named
|
|
827
854
|
# arguments cannot be forwarded — they would be silently dropped at call
|
|
@@ -832,10 +859,11 @@ def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
|
832
859
|
)
|
|
833
860
|
paginatable = op.paginatable
|
|
834
861
|
does_upload = op.does_upload
|
|
835
|
-
#
|
|
836
|
-
# with one
|
|
837
|
-
#
|
|
838
|
-
|
|
862
|
+
# asana-api's own flags (reserved uniformly across commands). An SDK arg/opt
|
|
863
|
+
# whose flag collides with one is exposed as ``--sdk-<name>`` (see
|
|
864
|
+
# ``_decls``) so the own flag keeps its bare name; the SDK param stays
|
|
865
|
+
# reachable + labelled.
|
|
866
|
+
reserved = _static_reserved_flags()
|
|
839
867
|
|
|
840
868
|
options: list[click.Option] = []
|
|
841
869
|
|
|
@@ -871,12 +899,17 @@ def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
|
871
899
|
# only affects multipart uploads, so it is exposed solely on upload commands
|
|
872
900
|
# (``does_upload``) rather than as a global flag. Off by default to preserve
|
|
873
901
|
# strict SDK parity (the SDK emits ``filename=`` only); see sdk-deviations.md.
|
|
902
|
+
# Its callback installs the patch and scopes it to this command via
|
|
903
|
+
# ``ctx.with_resource``; ``expose_value=False`` keeps it out of
|
|
904
|
+
# ``inner_callback``'s ``**kwargs``.
|
|
874
905
|
if does_upload:
|
|
875
906
|
options.append(
|
|
876
907
|
click.Option(
|
|
877
908
|
["--multibyte-filenames", "multibyte_filenames"],
|
|
878
909
|
is_flag=True,
|
|
879
910
|
default=False,
|
|
911
|
+
callback=_multibyte_filenames_callback,
|
|
912
|
+
expose_value=False,
|
|
880
913
|
help=(
|
|
881
914
|
"Emit RFC 5987 filename*=UTF-8'' on this multipart upload. "
|
|
882
915
|
"Required when the --file name contains non-ASCII characters; "
|
|
@@ -925,46 +958,10 @@ def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
|
925
958
|
header_params = kwargs.pop("header_params", None)
|
|
926
959
|
request_timeout = kwargs.pop("request_timeout", None)
|
|
927
960
|
|
|
928
|
-
#
|
|
929
|
-
#
|
|
930
|
-
#
|
|
931
|
-
|
|
932
|
-
if does_upload:
|
|
933
|
-
runtime.multibyte_filenames = kwargs.pop("multibyte_filenames", False)
|
|
934
|
-
|
|
935
|
-
# Deprecated aliases: pop from kwargs (per-command) and warn. Effective
|
|
936
|
-
# values fold into local vars without mutating ``runtime``, so the
|
|
937
|
-
# dispatch state stays scoped to this invocation.
|
|
938
|
-
all_items = kwargs.pop("all_items", False) if paginatable else False
|
|
939
|
-
page_size = kwargs.pop("page_size", None) if paginatable else None
|
|
940
|
-
max_items = kwargs.pop("max_items", None) if paginatable else None
|
|
941
|
-
|
|
942
|
-
effective_item_limit = item_limit
|
|
943
|
-
|
|
944
|
-
if all_items:
|
|
945
|
-
click.echo(
|
|
946
|
-
"warning: --all-items is deprecated; walking every page is "
|
|
947
|
-
"now the default (will be removed in a future release)",
|
|
948
|
-
err=True,
|
|
949
|
-
)
|
|
950
|
-
if page_size is not None:
|
|
951
|
-
click.echo(
|
|
952
|
-
"warning: --page-size is deprecated; use --limit instead "
|
|
953
|
-
"(will be removed in a future release)",
|
|
954
|
-
err=True,
|
|
955
|
-
)
|
|
956
|
-
# Canonical --limit wins when both are given.
|
|
957
|
-
if kwargs.get("limit") is None:
|
|
958
|
-
kwargs["limit"] = page_size
|
|
959
|
-
if max_items is not None:
|
|
960
|
-
click.echo(
|
|
961
|
-
"warning: --max-items is deprecated; use --item-limit "
|
|
962
|
-
"instead (will be removed in a future release)",
|
|
963
|
-
err=True,
|
|
964
|
-
)
|
|
965
|
-
# Canonical --item-limit wins when both are given.
|
|
966
|
-
if effective_item_limit is None:
|
|
967
|
-
effective_item_limit = max_items
|
|
961
|
+
# Deprecated aliases (--all-items / --page-size / --max-items): warn and
|
|
962
|
+
# fold into their canonical replacements. Isolated in a helper so their
|
|
963
|
+
# eventual removal does not touch this hot-path closure.
|
|
964
|
+
effective_item_limit = _apply_deprecated_aliases(kwargs, item_limit)
|
|
968
965
|
|
|
969
966
|
if op.has_body:
|
|
970
967
|
body_value = kwargs.pop("body") # click marks --body as required
|
|
@@ -1037,7 +1034,8 @@ def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
|
1037
1034
|
# Two independent layers:
|
|
1038
1035
|
# - Layer A (session lifecycle, above): every SDK call runs
|
|
1039
1036
|
# inside ``with AsanaSession.from_env() as session:``, which
|
|
1040
|
-
# keeps the ``HttpClientAuthRedactor`` installed
|
|
1037
|
+
# keeps the ``HttpClientAuthRedactor`` installed when ``--debug``
|
|
1038
|
+
# is active.
|
|
1041
1039
|
# - Layer B (this block): when the SDK returns a lazy iterator
|
|
1042
1040
|
# (PageIterator / EventIterator), iterating it issues one
|
|
1043
1041
|
# HTTP request per page. We must consume the iterator *before*
|
|
@@ -1053,12 +1051,11 @@ def _make_command(api_cls: type, op: _Operation) -> click.Command:
|
|
|
1053
1051
|
|
|
1054
1052
|
callback = formatted(inner_callback)
|
|
1055
1053
|
|
|
1056
|
-
#
|
|
1057
|
-
#
|
|
1058
|
-
#
|
|
1059
|
-
#
|
|
1060
|
-
|
|
1061
|
-
options.extend(fmt_params)
|
|
1054
|
+
# The output-formatting options (--output / --query / --csv-bom and their
|
|
1055
|
+
# error-path twins --exception-output / --exception-query) are declared by
|
|
1056
|
+
# ``make_formatter_options``; ``callback`` (wrapped by ``formatted``) consumes
|
|
1057
|
+
# their parsed values as kwargs.
|
|
1058
|
+
options.extend(make_formatter_options())
|
|
1062
1059
|
|
|
1063
1060
|
summary = _escape_help(op.summary or f"Call {api_cls.__name__}.{op.method_name}")
|
|
1064
1061
|
return CommandWithGlobalOptions(
|
|
@@ -1127,202 +1124,16 @@ _ROOT_EPILOG = (
|
|
|
1127
1124
|
)
|
|
1128
1125
|
|
|
1129
1126
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
"""
|
|
1138
|
-
if not _SDK_HAS_RETRY_STRATEGY:
|
|
1139
|
-
return f
|
|
1140
|
-
return click.option(
|
|
1141
|
-
"--retry-strategy",
|
|
1142
|
-
"retry_strategy_overrides",
|
|
1143
|
-
default=None,
|
|
1144
|
-
callback=click_callback(schema=RETRY_FIELD_SCHEMA),
|
|
1145
|
-
help=(
|
|
1146
|
-
"Override urllib3 Retry fields. VALUE: 'k1=v1,k2=v2,...', JSON "
|
|
1147
|
-
"object, or @path. See urllib3 Retry docs. List-typed fields "
|
|
1148
|
-
"(allowed_methods, status_forcelist, remove_headers_on_redirect) "
|
|
1149
|
-
"require JSON. (Configuration: retry_strategy)"
|
|
1150
|
-
),
|
|
1151
|
-
)(f)
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
# Root uses LazyGroup so that the manually declared @click.option globals are
|
|
1155
|
-
# not duplicated by GroupWithGlobalOptions' auto-append behavior. Subgroups
|
|
1156
|
-
# (_ApiGroup) and leaf commands (CommandWithGlobalOptions) still auto-append
|
|
1157
|
-
# so global options work at any level of the tree.
|
|
1127
|
+
# Root group. ``LazyGroup`` is a ``GroupWithGlobalOptions``, so the root
|
|
1128
|
+
# appends and consumes the global Configuration / ApiClient flags from the
|
|
1129
|
+
# single ``_global_option_sections`` source in ``click_ext.py`` — exactly the
|
|
1130
|
+
# way every subgroup (``_ApiGroup``) and leaf command (``CommandWithGlobalOptions``)
|
|
1131
|
+
# does. There is no separate root-level declaration; the flags work at any level
|
|
1132
|
+
# of the tree, and ``--retry-strategy`` is gated on the SDK version in that one
|
|
1133
|
+
# source.
|
|
1158
1134
|
@click.group(name="asana-api", cls=LazyGroup, epilog=_ROOT_EPILOG)
|
|
1159
1135
|
@click.version_option(version_string(), prog_name="asana-api")
|
|
1160
|
-
|
|
1161
|
-
"--host",
|
|
1162
|
-
default=None,
|
|
1163
|
-
help="Override API base URL (default: https://app.asana.com/api/1.0). (Configuration: host)",
|
|
1164
|
-
)
|
|
1165
|
-
@click.option("--proxy", default=None, help="HTTP/HTTPS proxy URL. (Configuration: proxy)")
|
|
1166
|
-
@click.option(
|
|
1167
|
-
"--verify-ssl/--no-verify-ssl",
|
|
1168
|
-
"verify_ssl",
|
|
1169
|
-
default=None,
|
|
1170
|
-
help=(
|
|
1171
|
-
"Verify TLS certificates (default: True). Pass --no-verify-ssl "
|
|
1172
|
-
"to disable (insecure). (Configuration: verify_ssl)"
|
|
1173
|
-
),
|
|
1174
|
-
)
|
|
1175
|
-
@click.option(
|
|
1176
|
-
"--ssl-ca-cert",
|
|
1177
|
-
"ssl_ca_cert",
|
|
1178
|
-
default=None,
|
|
1179
|
-
type=click.Path(exists=True, dir_okay=False),
|
|
1180
|
-
help="Path to a PEM bundle of trusted CA certs. (Configuration: ssl_ca_cert)",
|
|
1181
|
-
)
|
|
1182
|
-
@click.option(
|
|
1183
|
-
"--cert-file",
|
|
1184
|
-
"cert_file",
|
|
1185
|
-
default=None,
|
|
1186
|
-
type=click.Path(exists=True, dir_okay=False),
|
|
1187
|
-
help="Client TLS certificate for mTLS. (Configuration: cert_file)",
|
|
1188
|
-
)
|
|
1189
|
-
@click.option(
|
|
1190
|
-
"--key-file",
|
|
1191
|
-
"key_file",
|
|
1192
|
-
default=None,
|
|
1193
|
-
type=click.Path(exists=True, dir_okay=False),
|
|
1194
|
-
help="Client TLS private key for mTLS. (Configuration: key_file)",
|
|
1195
|
-
)
|
|
1196
|
-
@click.option(
|
|
1197
|
-
"--assert-hostname/--no-assert-hostname",
|
|
1198
|
-
"assert_hostname",
|
|
1199
|
-
default=None,
|
|
1200
|
-
help=(
|
|
1201
|
-
"Verify the server certificate's hostname matches the request URL "
|
|
1202
|
-
"host. Tri-state: unspecified → urllib3 default. "
|
|
1203
|
-
"(Configuration: assert_hostname)"
|
|
1204
|
-
),
|
|
1205
|
-
)
|
|
1206
|
-
@click.option(
|
|
1207
|
-
"--user-agent",
|
|
1208
|
-
"user_agent",
|
|
1209
|
-
default=None,
|
|
1210
|
-
help=("Override the User-Agent header the SDK sends on every request. (ApiClient: user_agent)"),
|
|
1211
|
-
)
|
|
1212
|
-
@click.option(
|
|
1213
|
-
"--set-default-header",
|
|
1214
|
-
"default_headers",
|
|
1215
|
-
multiple=True,
|
|
1216
|
-
callback=default_header_callback,
|
|
1217
|
-
help=(
|
|
1218
|
-
"Add an HTTP header sent on every request, given as NAME=VALUE; "
|
|
1219
|
-
"repeatable. Unlike per-call --header-params it applies to all "
|
|
1220
|
-
"calls. Not redacted in --debug output — see SECURITY.md. "
|
|
1221
|
-
"(ApiClient: set_default_header)"
|
|
1222
|
-
),
|
|
1223
|
-
)
|
|
1224
|
-
@_retry_strategy_option
|
|
1225
|
-
@click.option(
|
|
1226
|
-
"--connection-pool-maxsize",
|
|
1227
|
-
"connection_pool_maxsize",
|
|
1228
|
-
type=click.IntRange(min=1),
|
|
1229
|
-
default=None,
|
|
1230
|
-
help=(
|
|
1231
|
-
"Max urllib3 connections cached per host (default: cpu_count "
|
|
1232
|
-
"* 5). (Configuration: connection_pool_maxsize)"
|
|
1233
|
-
),
|
|
1234
|
-
)
|
|
1235
|
-
@click.option(
|
|
1236
|
-
"--access-token",
|
|
1237
|
-
"access_token",
|
|
1238
|
-
default=None,
|
|
1239
|
-
help=(
|
|
1240
|
-
"Asana personal access token (default: $ASANA_ACCESS_TOKEN). (Configuration: access_token)"
|
|
1241
|
-
),
|
|
1242
|
-
)
|
|
1243
|
-
@click.option(
|
|
1244
|
-
"--temp-folder-path",
|
|
1245
|
-
"temp_folder_path",
|
|
1246
|
-
default=None,
|
|
1247
|
-
type=click.Path(file_okay=False),
|
|
1248
|
-
help="Directory for temporary downloads. (Configuration: temp_folder_path)",
|
|
1249
|
-
)
|
|
1250
|
-
@click.option(
|
|
1251
|
-
"--safe-chars-for-path-param",
|
|
1252
|
-
"safe_chars_for_path_param",
|
|
1253
|
-
default=None,
|
|
1254
|
-
help=(
|
|
1255
|
-
"Extra chars treated as safe when percent-encoding path "
|
|
1256
|
-
"parameters. (Configuration: safe_chars_for_path_param)"
|
|
1257
|
-
),
|
|
1258
|
-
)
|
|
1259
|
-
@click.option(
|
|
1260
|
-
"--logger-format",
|
|
1261
|
-
"logger_format",
|
|
1262
|
-
default=None,
|
|
1263
|
-
help="Python logging format string. (Configuration: logger_format)",
|
|
1264
|
-
)
|
|
1265
|
-
@click.option(
|
|
1266
|
-
"--logger-file",
|
|
1267
|
-
"logger_file",
|
|
1268
|
-
default=None,
|
|
1269
|
-
type=click.Path(dir_okay=False),
|
|
1270
|
-
help="Path SDK loggers write to. (Configuration: logger_file)",
|
|
1271
|
-
)
|
|
1272
|
-
@click.option(
|
|
1273
|
-
"--debug",
|
|
1274
|
-
is_flag=True,
|
|
1275
|
-
default=False,
|
|
1276
|
-
help="Print HTTP request/response to stderr for troubleshooting. (Configuration: debug)",
|
|
1277
|
-
)
|
|
1278
|
-
@click.option(
|
|
1279
|
-
"--return-page-iterator/--no-return-page-iterator",
|
|
1280
|
-
"return_page_iterator",
|
|
1281
|
-
default=None,
|
|
1282
|
-
help=(
|
|
1283
|
-
"Toggle the SDK page iterator (default: enabled). With "
|
|
1284
|
-
"--no-return-page-iterator, paginatable endpoints return a "
|
|
1285
|
-
"single {data, next_page} dict from one HTTP call instead of "
|
|
1286
|
-
"auto-walking every page. (Configuration: return_page_iterator)"
|
|
1287
|
-
),
|
|
1288
|
-
)
|
|
1289
|
-
@click.option(
|
|
1290
|
-
"--page-limit",
|
|
1291
|
-
"page_limit",
|
|
1292
|
-
type=int,
|
|
1293
|
-
default=None,
|
|
1294
|
-
help=(
|
|
1295
|
-
"Per-page size when the iterator falls back to Configuration "
|
|
1296
|
-
"(default: 100). Equivalent to --limit on paginatable endpoints; "
|
|
1297
|
-
'--limit (per-call opts["limit"]) takes precedence when both '
|
|
1298
|
-
"are set. (Configuration: page_limit)"
|
|
1299
|
-
),
|
|
1300
|
-
)
|
|
1301
|
-
def main(
|
|
1302
|
-
host: str | None,
|
|
1303
|
-
proxy: str | None,
|
|
1304
|
-
verify_ssl: bool | None,
|
|
1305
|
-
ssl_ca_cert: str | None,
|
|
1306
|
-
cert_file: str | None,
|
|
1307
|
-
key_file: str | None,
|
|
1308
|
-
assert_hostname: bool | None,
|
|
1309
|
-
user_agent: str | None,
|
|
1310
|
-
default_headers: dict[str, str] | None,
|
|
1311
|
-
# ``retry_strategy_overrides`` and everything after it have ``= None``
|
|
1312
|
-
# defaults so the ``--retry-strategy`` decorator can be skipped on
|
|
1313
|
-
# python-asana <5.1 without click then trying to call this function
|
|
1314
|
-
# without a value for that name.
|
|
1315
|
-
retry_strategy_overrides: dict[str, Any] | None = None,
|
|
1316
|
-
connection_pool_maxsize: int | None = None,
|
|
1317
|
-
access_token: str | None = None,
|
|
1318
|
-
temp_folder_path: str | None = None,
|
|
1319
|
-
safe_chars_for_path_param: str | None = None,
|
|
1320
|
-
logger_format: str | None = None,
|
|
1321
|
-
logger_file: str | None = None,
|
|
1322
|
-
debug: bool = False,
|
|
1323
|
-
return_page_iterator: bool | None = None,
|
|
1324
|
-
page_limit: int | None = None,
|
|
1325
|
-
) -> None:
|
|
1136
|
+
def main() -> None:
|
|
1326
1137
|
"""Asana API CLI — runtime-introspected wrapper around the python-asana SDK."""
|
|
1327
1138
|
# JSON I/O is required to be UTF-8 by RFC 8259, but on Windows the default
|
|
1328
1139
|
# stream encodings are the locale code page (e.g. cp932 on Japanese
|
|
@@ -1331,39 +1142,15 @@ def main(
|
|
|
1331
1142
|
# Reconfigure all three to UTF-8 so the same input/output works on every
|
|
1332
1143
|
# platform. The hasattr guard keeps CliRunner's in-memory streams (used
|
|
1333
1144
|
# by tests) from blowing up, since StringIO has no reconfigure().
|
|
1145
|
+
#
|
|
1146
|
+
# The global flags are parsed into the root context and written to
|
|
1147
|
+
# ``runtime`` by ``GroupWithGlobalOptions.invoke`` → ``_consume_global_options``
|
|
1148
|
+
# (inherited by ``LazyGroup``) before this callback runs, so there is nothing
|
|
1149
|
+
# to apply here.
|
|
1334
1150
|
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
1335
1151
|
if hasattr(stream, "reconfigure"):
|
|
1336
1152
|
stream.reconfigure(encoding="utf-8") # pyright: ignore[reportAttributeAccessIssue]
|
|
1337
1153
|
|
|
1338
|
-
runtime.host = host
|
|
1339
|
-
runtime.proxy = proxy
|
|
1340
|
-
# Both verify_ssl and assert_hostname are tri-state toggles (positive /
|
|
1341
|
-
# negative / unset). Guard so the unset case does not clobber a value
|
|
1342
|
-
# set by an earlier code path; symmetry with how the leaf-level
|
|
1343
|
-
# propagation in ``click_ext._consume_global_options`` skips the
|
|
1344
|
-
# default ``None``.
|
|
1345
|
-
if verify_ssl is not None:
|
|
1346
|
-
runtime.verify_ssl = verify_ssl
|
|
1347
|
-
runtime.ssl_ca_cert = ssl_ca_cert
|
|
1348
|
-
runtime.cert_file = cert_file
|
|
1349
|
-
runtime.key_file = key_file
|
|
1350
|
-
if assert_hostname is not None:
|
|
1351
|
-
runtime.assert_hostname = assert_hostname
|
|
1352
|
-
runtime.user_agent = user_agent
|
|
1353
|
-
runtime.default_headers = default_headers
|
|
1354
|
-
runtime.retry_strategy_overrides = retry_strategy_overrides
|
|
1355
|
-
runtime.connection_pool_maxsize = connection_pool_maxsize
|
|
1356
|
-
if access_token:
|
|
1357
|
-
runtime.access_token = access_token
|
|
1358
|
-
runtime.temp_folder_path = temp_folder_path
|
|
1359
|
-
runtime.safe_chars_for_path_param = safe_chars_for_path_param
|
|
1360
|
-
runtime.logger_format = logger_format
|
|
1361
|
-
runtime.logger_file = logger_file
|
|
1362
|
-
runtime.debug = debug
|
|
1363
|
-
if return_page_iterator is not None:
|
|
1364
|
-
runtime.return_page_iterator = return_page_iterator
|
|
1365
|
-
runtime.page_limit = page_limit
|
|
1366
|
-
|
|
1367
1154
|
|
|
1368
1155
|
def _register_groups(root: click.Group) -> None:
|
|
1369
1156
|
for cls in _enumerate_api_classes():
|