asana-api-cli 1.2.0__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.
- asana_api_cli/__init__.py +3 -0
- asana_api_cli/cli/__init__.py +140 -0
- asana_api_cli/cli/access_requests.py +66 -0
- asana_api_cli/cli/allocations.py +101 -0
- asana_api_cli/cli/attachments.py +92 -0
- asana_api_cli/cli/audit_log_api.py +52 -0
- asana_api_cli/cli/batch_api.py +30 -0
- asana_api_cli/cli/budgets.py +79 -0
- asana_api_cli/cli/custom_field_settings.py +92 -0
- asana_api_cli/cli/custom_fields.py +133 -0
- asana_api_cli/cli/custom_types.py +50 -0
- asana_api_cli/cli/events.py +32 -0
- asana_api_cli/cli/exports.py +39 -0
- asana_api_cli/cli/goal_relationships.py +98 -0
- asana_api_cli/cli/goals.py +217 -0
- asana_api_cli/cli/jobs.py +29 -0
- asana_api_cli/cli/memberships.py +89 -0
- asana_api_cli/cli/organization_exports.py +44 -0
- asana_api_cli/cli/portfolio_memberships.py +83 -0
- asana_api_cli/cli/portfolios.py +215 -0
- asana_api_cli/cli/project_briefs.py +72 -0
- asana_api_cli/cli/project_memberships.py +53 -0
- asana_api_cli/cli/project_portfolio_settings.py +87 -0
- asana_api_cli/cli/project_statuses.py +77 -0
- asana_api_cli/cli/project_templates.py +102 -0
- asana_api_cli/cli/projects.py +380 -0
- asana_api_cli/cli/rates.py +97 -0
- asana_api_cli/cli/reactions.py +34 -0
- asana_api_cli/cli/roles.py +98 -0
- asana_api_cli/cli/rules.py +28 -0
- asana_api_cli/cli/sections.py +111 -0
- asana_api_cli/cli/status_updates.py +86 -0
- asana_api_cli/cli/stories.py +130 -0
- asana_api_cli/cli/tags.py +155 -0
- asana_api_cli/cli/task_templates.py +77 -0
- asana_api_cli/cli/tasks.py +520 -0
- asana_api_cli/cli/team_memberships.py +103 -0
- asana_api_cli/cli/teams.py +133 -0
- asana_api_cli/cli/time_periods.py +57 -0
- asana_api_cli/cli/time_tracking_categories.py +123 -0
- asana_api_cli/cli/time_tracking_entries.py +138 -0
- asana_api_cli/cli/timesheet_approval_statuses.py +94 -0
- asana_api_cli/cli/typeahead.py +40 -0
- asana_api_cli/cli/user_task_lists.py +45 -0
- asana_api_cli/cli/users.py +173 -0
- asana_api_cli/cli/webhooks.py +96 -0
- asana_api_cli/cli/workspace_memberships.py +75 -0
- asana_api_cli/cli/workspaces.py +113 -0
- asana_api_cli/formatter.py +161 -0
- asana_api_cli/session.py +173 -0
- asana_api_cli/version.py +11 -0
- asana_api_cli-1.2.0.dist-info/METADATA +105 -0
- asana_api_cli-1.2.0.dist-info/RECORD +57 -0
- asana_api_cli-1.2.0.dist-info/WHEEL +5 -0
- asana_api_cli-1.2.0.dist-info/entry_points.txt +2 -0
- asana_api_cli-1.2.0.dist-info/licenses/LICENSE +190 -0
- asana_api_cli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import csv
|
|
5
|
+
import functools
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import jq as jqlib
|
|
13
|
+
from asana.rest import ApiException
|
|
14
|
+
from tabulate import tabulate
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def formatted(f: Any) -> Any:
|
|
18
|
+
"""Decorator that adds --output / --query and auto-formats the returned dict."""
|
|
19
|
+
|
|
20
|
+
@click.option(
|
|
21
|
+
"--output",
|
|
22
|
+
"output_format",
|
|
23
|
+
type=click.Choice(["json", "table", "csv", "text"], case_sensitive=False),
|
|
24
|
+
default="json",
|
|
25
|
+
help="Output format (default: json)",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--query",
|
|
29
|
+
"jq_query",
|
|
30
|
+
default=None,
|
|
31
|
+
help="jq expression to filter output",
|
|
32
|
+
)
|
|
33
|
+
@functools.wraps(f)
|
|
34
|
+
def wrapper(*args: Any, output_format: str, jq_query: str | None, **kwargs: Any) -> None:
|
|
35
|
+
try:
|
|
36
|
+
data = f(*args, **kwargs)
|
|
37
|
+
# Collapse the asana SDK PageIterator / generator into a list
|
|
38
|
+
if not isinstance(data, (dict, list, str, int, float, bool, type(None))):
|
|
39
|
+
with contextlib.suppress(TypeError):
|
|
40
|
+
data = list(data)
|
|
41
|
+
except ApiException as e:
|
|
42
|
+
_handle_api_exception(e)
|
|
43
|
+
_format_output(data, output_format=output_format, jq_query=jq_query)
|
|
44
|
+
|
|
45
|
+
return wrapper
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _handle_api_exception(e: ApiException) -> None:
|
|
49
|
+
"""Print an Asana API error in human-readable form and exit."""
|
|
50
|
+
from asana_api_cli.session import runtime
|
|
51
|
+
|
|
52
|
+
status = e.status or "error"
|
|
53
|
+
messages: list[str] = []
|
|
54
|
+
body = e.body
|
|
55
|
+
if isinstance(body, bytes):
|
|
56
|
+
with contextlib.suppress(UnicodeDecodeError):
|
|
57
|
+
body = body.decode("utf-8")
|
|
58
|
+
if isinstance(body, str):
|
|
59
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
60
|
+
payload = json.loads(body)
|
|
61
|
+
if isinstance(payload, dict):
|
|
62
|
+
for err in payload.get("errors") or []:
|
|
63
|
+
if isinstance(err, dict) and "message" in err:
|
|
64
|
+
messages.append(str(err["message"]))
|
|
65
|
+
if not messages:
|
|
66
|
+
messages.append(e.reason or "Unknown API error")
|
|
67
|
+
for msg in messages:
|
|
68
|
+
click.echo(f"Error ({status}): {msg}", err=True)
|
|
69
|
+
# When the body was not JSON, show a hint and,
|
|
70
|
+
# in debug mode, dump the raw body so the user can diagnose the issue.
|
|
71
|
+
if isinstance(body, str) and body and not _is_json(body):
|
|
72
|
+
click.echo(
|
|
73
|
+
"The server returned a non-JSON response. "
|
|
74
|
+
"Re-run with --debug to see the full response body.",
|
|
75
|
+
err=True,
|
|
76
|
+
)
|
|
77
|
+
if runtime.debug:
|
|
78
|
+
click.echo("--- raw response body ---", err=True)
|
|
79
|
+
click.echo(body, err=True)
|
|
80
|
+
click.echo("--- end of response body ---", err=True)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_json(text: str) -> bool:
|
|
85
|
+
"""Return True if *text* looks like JSON."""
|
|
86
|
+
try:
|
|
87
|
+
json.loads(text)
|
|
88
|
+
except (json.JSONDecodeError, ValueError):
|
|
89
|
+
return False
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _format_output(data: Any, *, output_format: str, jq_query: str | None) -> None:
|
|
94
|
+
if jq_query:
|
|
95
|
+
try:
|
|
96
|
+
data = jqlib.first(jq_query, data)
|
|
97
|
+
except ValueError as e:
|
|
98
|
+
click.echo(f"Invalid jq expression: {e}", err=True)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
if output_format == "text":
|
|
102
|
+
_print_text(data)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if output_format == "json":
|
|
106
|
+
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
rows = _to_rows(data)
|
|
110
|
+
if rows is None:
|
|
111
|
+
click.echo(data)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if output_format == "table":
|
|
115
|
+
click.echo(tabulate(rows, headers="keys", tablefmt="simple"))
|
|
116
|
+
elif output_format == "csv":
|
|
117
|
+
_print_csv(rows)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _to_rows(data: Any) -> list[dict[str, Any]] | None:
|
|
121
|
+
"""Convert data into a list of dicts for table/csv. Return None if not possible."""
|
|
122
|
+
if isinstance(data, list):
|
|
123
|
+
if not data:
|
|
124
|
+
return []
|
|
125
|
+
if isinstance(data[0], dict):
|
|
126
|
+
return data
|
|
127
|
+
return [{"value": v} for v in data]
|
|
128
|
+
if isinstance(data, dict):
|
|
129
|
+
return [data]
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _print_text(data: Any) -> None:
|
|
134
|
+
"""Print data in plain text format (like ``aws --output text``)."""
|
|
135
|
+
if data is None:
|
|
136
|
+
click.echo("None")
|
|
137
|
+
return
|
|
138
|
+
if isinstance(data, (str, int, float, bool)):
|
|
139
|
+
click.echo(data)
|
|
140
|
+
return
|
|
141
|
+
if isinstance(data, dict):
|
|
142
|
+
click.echo("\t".join(str(v) for v in data.values()))
|
|
143
|
+
return
|
|
144
|
+
if isinstance(data, list):
|
|
145
|
+
for item in data:
|
|
146
|
+
if isinstance(item, dict):
|
|
147
|
+
click.echo("\t".join(str(v) for v in item.values()))
|
|
148
|
+
else:
|
|
149
|
+
click.echo(item)
|
|
150
|
+
return
|
|
151
|
+
click.echo(data)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _print_csv(rows: list[dict[str, Any]]) -> None:
|
|
155
|
+
if not rows:
|
|
156
|
+
return
|
|
157
|
+
buf = io.StringIO()
|
|
158
|
+
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
|
|
159
|
+
writer.writeheader()
|
|
160
|
+
writer.writerows(rows)
|
|
161
|
+
click.echo(buf.getvalue(), nl=False)
|
asana_api_cli/session.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Asana SDK client construction utilities.
|
|
2
|
+
|
|
3
|
+
A thin wrapper around the official `asana` SDK ApiClient that handles
|
|
4
|
+
initialization from environment variables, toggling pagination mode, and
|
|
5
|
+
applying the global configuration passed in from the CLI.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import asana
|
|
18
|
+
from urllib3.util.retry import Retry
|
|
19
|
+
|
|
20
|
+
DEFAULT_TOKEN_ENV = "ASANA_ACCESS_TOKEN"
|
|
21
|
+
DEFAULT_WORKSPACE_ENV = "ASANA_DEFAULT_WORKSPACE"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_body(value: str) -> Any:
|
|
25
|
+
"""Parse a body argument as JSON.
|
|
26
|
+
|
|
27
|
+
Supports three input forms:
|
|
28
|
+
- ``@path`` — read JSON from a file
|
|
29
|
+
- ``-`` — read JSON from stdin
|
|
30
|
+
- otherwise — parse the string itself as JSON
|
|
31
|
+
"""
|
|
32
|
+
if value == "-":
|
|
33
|
+
raw = sys.stdin.read()
|
|
34
|
+
elif value.startswith("@"):
|
|
35
|
+
path = Path(value[1:])
|
|
36
|
+
try:
|
|
37
|
+
raw = path.read_text(encoding="utf-8")
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
print(f"Body file not found: {path}", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
except OSError as exc:
|
|
42
|
+
print(f"Cannot read body file {path}: {exc}", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
else:
|
|
45
|
+
raw = value
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(raw)
|
|
49
|
+
except json.JSONDecodeError as exc:
|
|
50
|
+
print(f"Invalid JSON in body: {exc}", file=sys.stderr)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class _Runtime:
|
|
56
|
+
"""Configuration shared globally during a CLI invocation. Updated by the main group callback."""
|
|
57
|
+
|
|
58
|
+
debug: bool = False
|
|
59
|
+
host: str | None = None
|
|
60
|
+
proxy: str | None = None
|
|
61
|
+
verify_ssl: bool = True
|
|
62
|
+
ssl_ca_cert: str | None = None
|
|
63
|
+
page_limit: int | None = None
|
|
64
|
+
retries: int | None = None
|
|
65
|
+
timeout: float | None = None
|
|
66
|
+
token_env: str = DEFAULT_TOKEN_ENV
|
|
67
|
+
temp_dir: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
runtime = _Runtime()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AsanaSession:
|
|
74
|
+
"""Session that holds an ApiClient from the official asana SDK."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, token: str, *, paginate: bool = False) -> None:
|
|
77
|
+
config = asana.Configuration()
|
|
78
|
+
config.access_token = token
|
|
79
|
+
# When --paginate is set, the SDK returns a PageIterator that walks every page.
|
|
80
|
+
config.return_page_iterator = paginate
|
|
81
|
+
|
|
82
|
+
# Apply runtime values to Configuration
|
|
83
|
+
if runtime.host:
|
|
84
|
+
config.host = runtime.host
|
|
85
|
+
if runtime.proxy:
|
|
86
|
+
config.proxy = runtime.proxy
|
|
87
|
+
if not runtime.verify_ssl:
|
|
88
|
+
config.verify_ssl = False
|
|
89
|
+
if runtime.ssl_ca_cert:
|
|
90
|
+
config.ssl_ca_cert = runtime.ssl_ca_cert
|
|
91
|
+
if runtime.page_limit is not None:
|
|
92
|
+
config.page_limit = runtime.page_limit
|
|
93
|
+
if runtime.temp_dir:
|
|
94
|
+
config.temp_folder_path = runtime.temp_dir
|
|
95
|
+
if runtime.retries is not None:
|
|
96
|
+
# Replace only `total` while keeping the existing backoff/status_forcelist.
|
|
97
|
+
config.retry_strategy = Retry(
|
|
98
|
+
total=runtime.retries,
|
|
99
|
+
backoff_factor=2,
|
|
100
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
101
|
+
)
|
|
102
|
+
if runtime.debug:
|
|
103
|
+
# The SDK debug setter attaches a stderr handler to the urllib3/asana
|
|
104
|
+
# loggers and enables http.client.HTTPConnection.debuglevel.
|
|
105
|
+
config.debug = True
|
|
106
|
+
|
|
107
|
+
self._config = config
|
|
108
|
+
self._client = asana.ApiClient(config)
|
|
109
|
+
|
|
110
|
+
# Configuration has no --timeout knob, so wrap call_api to inject it.
|
|
111
|
+
if runtime.timeout is not None:
|
|
112
|
+
self._install_timeout(runtime.timeout)
|
|
113
|
+
|
|
114
|
+
def _install_timeout(self, timeout: float) -> None:
|
|
115
|
+
"""Wrap ApiClient.call_api to inject a default _request_timeout."""
|
|
116
|
+
original = self._client.call_api
|
|
117
|
+
|
|
118
|
+
@functools.wraps(original)
|
|
119
|
+
def call_api_with_timeout(*args: Any, **kwargs: Any) -> Any:
|
|
120
|
+
kwargs.setdefault("_request_timeout", timeout)
|
|
121
|
+
return original(*args, **kwargs)
|
|
122
|
+
|
|
123
|
+
self._client.call_api = call_api_with_timeout # type: ignore[method-assign]
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def client(self) -> asana.ApiClient:
|
|
127
|
+
return self._client
|
|
128
|
+
|
|
129
|
+
def api(self, api_class: type) -> object:
|
|
130
|
+
"""Take a <Tag>Api class and return an instance bound to this session."""
|
|
131
|
+
return api_class(self._client)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_env(cls, *, paginate: bool = False) -> "AsanaSession":
|
|
135
|
+
"""Build a session from environment variables (variable name from runtime.token_env)."""
|
|
136
|
+
var = runtime.token_env or DEFAULT_TOKEN_ENV
|
|
137
|
+
token = os.environ.get(var, "")
|
|
138
|
+
if not token:
|
|
139
|
+
print(f"{var} environment variable is not set", file=sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
return cls(token=token, paginate=paginate)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_workspace(
|
|
145
|
+
explicit: str | None,
|
|
146
|
+
*,
|
|
147
|
+
required: bool = False,
|
|
148
|
+
) -> str | None:
|
|
149
|
+
"""Resolve workspace GID with fallback chain.
|
|
150
|
+
|
|
151
|
+
Priority: explicit ``--workspace`` value > ``ASANA_DEFAULT_WORKSPACE``
|
|
152
|
+
env var (only when *required* is True).
|
|
153
|
+
|
|
154
|
+
When workspace is optional (``required=False``), the env-var fallback is
|
|
155
|
+
**not** used. This prevents the default workspace from being sent
|
|
156
|
+
alongside other scope parameters (e.g. ``--project`` on ``get-tasks``)
|
|
157
|
+
that are mutually exclusive with workspace in the Asana API.
|
|
158
|
+
|
|
159
|
+
If *required* is True and no value is found, exits with an error.
|
|
160
|
+
"""
|
|
161
|
+
if explicit is not None:
|
|
162
|
+
return explicit
|
|
163
|
+
if required:
|
|
164
|
+
ws = os.environ.get(DEFAULT_WORKSPACE_ENV)
|
|
165
|
+
if ws:
|
|
166
|
+
return ws
|
|
167
|
+
print(
|
|
168
|
+
f"Workspace is required. Specify --workspace or "
|
|
169
|
+
f"set {DEFAULT_WORKSPACE_ENV}.",
|
|
170
|
+
file=sys.stderr,
|
|
171
|
+
)
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
return None
|
asana_api_cli/version.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Version information for the CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def version_string() -> str:
|
|
8
|
+
"""Return a version string including the python-asana SDK version."""
|
|
9
|
+
cli_ver = version("asana-api-cli")
|
|
10
|
+
sdk_ver = version("asana")
|
|
11
|
+
return f"{cli_ver} (python-asana {sdk_ver})"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asana-api-cli
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Command-line wrapper around the official Asana Python SDK
|
|
5
|
+
Author-email: Masanao Izumo <asana@masanao.site>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/izumo-m/asana-api-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/izumo-m/asana-api-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/izumo-m/asana-api-cli/issues
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: click<9,>=8.1
|
|
14
|
+
Requires-Dist: jq<2,>=1.8
|
|
15
|
+
Requires-Dist: tabulate<1,>=0.9
|
|
16
|
+
Requires-Dist: asana<6,>=5.2.4
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# asana-api-cli
|
|
20
|
+
|
|
21
|
+
A CLI tool for the Asana API. It thinly wraps the official
|
|
22
|
+
[python-asana](https://github.com/Asana/python-asana) SDK with click, exposing
|
|
23
|
+
every API endpoint from the command line via `asana-api <group> <command>`.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install asana-api-cli
|
|
29
|
+
|
|
30
|
+
# or, to install as an isolated CLI tool
|
|
31
|
+
pipx install asana-api-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Environment variables
|
|
35
|
+
|
|
36
|
+
| Name | Required | Description |
|
|
37
|
+
|------|----------|-------------|
|
|
38
|
+
| `ASANA_ACCESS_TOKEN` | Yes (at runtime only) | Asana Personal Access Token |
|
|
39
|
+
| `ASANA_DEFAULT_WORKSPACE` | No | Default workspace GID for endpoints that require it |
|
|
40
|
+
|
|
41
|
+
The token can be issued from the
|
|
42
|
+
[Asana Developer Console](https://app.asana.com/0/developer-console).
|
|
43
|
+
No token is needed for `--help` or argument-error output.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export ASANA_ACCESS_TOKEN="1/12345..."
|
|
47
|
+
export ASANA_DEFAULT_WORKSPACE="12345678" # optional
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Show version
|
|
54
|
+
asana-api --version
|
|
55
|
+
|
|
56
|
+
# List commands
|
|
57
|
+
asana-api --help
|
|
58
|
+
asana-api tasks --help
|
|
59
|
+
asana-api tasks get-tasks --help
|
|
60
|
+
|
|
61
|
+
# List workspaces
|
|
62
|
+
asana-api workspaces get-workspaces
|
|
63
|
+
|
|
64
|
+
# List projects (workspace resolved from ASANA_DEFAULT_WORKSPACE)
|
|
65
|
+
asana-api projects get-projects-for-workspace
|
|
66
|
+
asana-api projects get-projects --workspace <WORKSPACE_GID>
|
|
67
|
+
|
|
68
|
+
# List tasks (first page)
|
|
69
|
+
asana-api tasks get-tasks --project <PROJECT_GID>
|
|
70
|
+
|
|
71
|
+
# Auto-fetch all pages
|
|
72
|
+
asana-api tasks get-tasks --project <PROJECT_GID> --paginate
|
|
73
|
+
|
|
74
|
+
# Single task (--task instead of positional argument)
|
|
75
|
+
asana-api tasks get-task --task <TASK_GID>
|
|
76
|
+
|
|
77
|
+
# Create a task (body is a JSON string)
|
|
78
|
+
asana-api tasks create-task --body '{"data":{"name":"new task","projects":["<PID>"]}}'
|
|
79
|
+
|
|
80
|
+
# Output formats
|
|
81
|
+
asana-api tasks get-tasks --project <PID> --output table
|
|
82
|
+
asana-api tasks get-tasks --project <PID> --query '.data' --output csv
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Workspace resolution
|
|
86
|
+
|
|
87
|
+
Many API endpoints require a workspace. For those endpoints (e.g.
|
|
88
|
+
`get-projects-for-workspace`), the CLI resolves it in this order:
|
|
89
|
+
|
|
90
|
+
1. `--workspace <GID>` on the command
|
|
91
|
+
2. `ASANA_DEFAULT_WORKSPACE` environment variable
|
|
92
|
+
|
|
93
|
+
For endpoints where workspace is optional (e.g. `get-tasks`), the env-var
|
|
94
|
+
fallback is **not** used — pass `--workspace` explicitly if needed. This
|
|
95
|
+
prevents conflicts with other scope parameters like `--project` that are
|
|
96
|
+
mutually exclusive with workspace in the Asana API.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
See [docs/development.md](https://github.com/izumo-m/asana-api-cli/blob/main/docs/development.md)
|
|
101
|
+
for building from source, project layout, and library usage.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
[Apache License 2.0](https://github.com/izumo-m/asana-api-cli/blob/main/LICENSE)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
asana_api_cli/__init__.py,sha256=VwCvAdoxu2DuaYGGMVBTXB-DgPagEeKIpop-BaiLL7k,75
|
|
2
|
+
asana_api_cli/formatter.py,sha256=V5-rfMtMwcAbW-P0KPGa05RKUWOjHUkpXsXibmDfVok,4965
|
|
3
|
+
asana_api_cli/session.py,sha256=s3f01vk1LE7U4wO_7rdKgO7XkeoVjeuC0WnQZC6qEg4,5746
|
|
4
|
+
asana_api_cli/version.py,sha256=Hhyl20g8y07oEoBHBhJrNre92HlEkVV_kdDnvKxXsh0,338
|
|
5
|
+
asana_api_cli/cli/__init__.py,sha256=0MjxSiKGnHcWUWuqEucGLBvXPQk7TQUjc9VTtyoFeo0,6711
|
|
6
|
+
asana_api_cli/cli/access_requests.py,sha256=XZxooEyLKjpcWalqvakfcdPx6dZzQypNTZqn_htzVyc,2851
|
|
7
|
+
asana_api_cli/cli/allocations.py,sha256=l6TRGWd9TTi05_Nona5wYorT9bbjgtboUJ3cZbn9jaY,5236
|
|
8
|
+
asana_api_cli/cli/attachments.py,sha256=19kyRq-Bzt4c6ajjJNvoIl-PD1JLpx5C4o7Lg4c2N1M,4603
|
|
9
|
+
asana_api_cli/cli/audit_log_api.py,sha256=OJxWaTqtVySR22k05sgIAD_vH4k0wQVmGErDVl6Qb9c,2960
|
|
10
|
+
asana_api_cli/cli/batch_api.py,sha256=oukiisYeU-eXrVCo808-bW5Ljqinz5GOVvd953IeE70,1178
|
|
11
|
+
asana_api_cli/cli/budgets.py,sha256=aw3WcZgaEqMOYzT4l6yZwyBFe0kVtoLcXO7b3OY03ec,3245
|
|
12
|
+
asana_api_cli/cli/custom_field_settings.py,sha256=73MeEuTIzXmX93rUp7OXJLdT0IkNOW376nKta0e35dI,5709
|
|
13
|
+
asana_api_cli/cli/custom_fields.py,sha256=hxXS7fPNQE0E0OvfS9020_8d7yjJsftROslo1oeb07Q,7132
|
|
14
|
+
asana_api_cli/cli/custom_types.py,sha256=vahRL3JhDm8Q4m96dcSpG2spAySa9jFijVSzhL0iXEU,2639
|
|
15
|
+
asana_api_cli/cli/events.py,sha256=DesmksXsaf2keXF-pJuuZsHGY5TEXkh7snVIw7g5p68,1446
|
|
16
|
+
asana_api_cli/cli/exports.py,sha256=iGYghD6XbLskieJvcdtbzFGujioBPiHJaGltp6qFq64,1442
|
|
17
|
+
asana_api_cli/cli/goal_relationships.py,sha256=67S3g7raB3-Y3w8iBlQC7mikJiRNx1aKp3VOFyMGBRY,5544
|
|
18
|
+
asana_api_cli/cli/goals.py,sha256=6hlmyNFIW2h4Hun96Plun7ypjzcMts5tDO6xet6wW5c,11206
|
|
19
|
+
asana_api_cli/cli/jobs.py,sha256=zhLLxYuUXgVebOjNFTL5KtidoIlTmcoxQZqICLPnjb8,1046
|
|
20
|
+
asana_api_cli/cli/memberships.py,sha256=Xbi2Ax1P6eg8joN7PGgASKiyqOk-1WMXWVpPNvgXRwM,4443
|
|
21
|
+
asana_api_cli/cli/organization_exports.py,sha256=h00T2xWvrJmtA84RNM8D6UuCadhnvxtAAD7B36qJMTk,2087
|
|
22
|
+
asana_api_cli/cli/portfolio_memberships.py,sha256=2gyih0Wt0_Oq7q5Jbi_b3_67pC1r7m2fZxmelWBI3iA,4961
|
|
23
|
+
asana_api_cli/cli/portfolios.py,sha256=jsxR9nwQ3VE1EqGXYVLxPKOulPpUJJpLDLI3WwoloG4,11701
|
|
24
|
+
asana_api_cli/cli/project_briefs.py,sha256=POnb6UUQukVQ-B1_X904s8-HcMeZTVGEp-0gMMX2nBw,3479
|
|
25
|
+
asana_api_cli/cli/project_memberships.py,sha256=0LutTGJA4vkboniOpU6tu97JLKFZD19wWCakhpBfR7M,2885
|
|
26
|
+
asana_api_cli/cli/project_portfolio_settings.py,sha256=TXjv8YGnHxoYHGWx80cHvQZrhj_oEsA0EkxyCvrOURU,5480
|
|
27
|
+
asana_api_cli/cli/project_statuses.py,sha256=11L1RBGLX2v6PBvw1vCbVLwOUxVg7ndzJV8JLSR47ak,4064
|
|
28
|
+
asana_api_cli/cli/project_templates.py,sha256=YwKAsqhmE9ZvynoeM64CCuvFUz5FDRccMSW0pReDAEs,5760
|
|
29
|
+
asana_api_cli/cli/projects.py,sha256=9BTLESTDi5HXc4b0qy5hKokbNXYmd9nYlwXemnAtjpo,21997
|
|
30
|
+
asana_api_cli/cli/rates.py,sha256=lLSDhEiCSIGAEsyMyS_9GN1FhUXanU3VfvrDx8M9B2A,4603
|
|
31
|
+
asana_api_cli/cli/reactions.py,sha256=Gqof-upq3ton7Bi0-3LlJ5c12ibEMXec8VIngwP6Nho,1690
|
|
32
|
+
asana_api_cli/cli/roles.py,sha256=Wk4_RPNqmS9IJjvHXWFDAVWtWlxoEYTPJiM5ibGv4zU,4756
|
|
33
|
+
asana_api_cli/cli/rules.py,sha256=hlbQV97Mma-1wXyUtHmg9K0Z6d20gDhSg_ADnM_mC38,1063
|
|
34
|
+
asana_api_cli/cli/sections.py,sha256=hoqDuzbDb4I9lV41OAFGPbwVyiZYknKltiy020pntz0,5249
|
|
35
|
+
asana_api_cli/cli/status_updates.py,sha256=549jdi_rdTi0tfvpg8KK5I8sv0rO5bmvBIaPplUnGpg,4764
|
|
36
|
+
asana_api_cli/cli/stories.py,sha256=NgeiVl5yZNyKwJXgtOKciqV8rRkZzW41w5PNg-CDfmE,6803
|
|
37
|
+
asana_api_cli/cli/tags.py,sha256=he7LMNsQ2TbGyJOZSHqpIVa7mAJOEoE7CulEkrZctM4,8310
|
|
38
|
+
asana_api_cli/cli/task_templates.py,sha256=ct2H4NcDFaXCMuOnNvioRLanZx83z6PezyUwFnmmMVE,3923
|
|
39
|
+
asana_api_cli/cli/tasks.py,sha256=u-p-E6tWzARDx8npli4NK3hwSFki1I1QtPfYrUrkc50,29615
|
|
40
|
+
asana_api_cli/cli/team_memberships.py,sha256=kmti-zH5Vh3Mr0zDJbIkXt9zkh_V23wQHTmJxhpIXyo,6335
|
|
41
|
+
asana_api_cli/cli/teams.py,sha256=3pEaF2A-AgPPbzoTcWA2ruLfbudW-y-iadBbIkhB1QM,7103
|
|
42
|
+
asana_api_cli/cli/time_periods.py,sha256=E55GFasCeHqV0ycIbsGV3RnZLyrYxY9Og6iZhOuife4,2955
|
|
43
|
+
asana_api_cli/cli/time_tracking_categories.py,sha256=PUUpBzaC5YRj0swh4oB8suBNYL9-4tlaQBMI-lI43pY,7651
|
|
44
|
+
asana_api_cli/cli/time_tracking_entries.py,sha256=AtSgSdwWwEeIOKHz0DROy15jgC7BSwlzX6Q_Zmek148,8543
|
|
45
|
+
asana_api_cli/cli/timesheet_approval_statuses.py,sha256=FV3wGdeVQdLzpL4Fp8tzHs9ryp4L2kq-V8sZfjL85l4,5720
|
|
46
|
+
asana_api_cli/cli/typeahead.py,sha256=0Cw24ttyoTIvQ56omb03_oPxTh7tkAB8md67D3dTEZ4,2300
|
|
47
|
+
asana_api_cli/cli/user_task_lists.py,sha256=FUqnOk_t31cAk7hrXm_9Om-uV6NaUaWhectZa9jPitU,2224
|
|
48
|
+
asana_api_cli/cli/users.py,sha256=E62aDKkBjirx9Xp4bM8-y7NA9FiPC1NCizxGP2qCF-I,10462
|
|
49
|
+
asana_api_cli/cli/webhooks.py,sha256=Wv7M1gTJn1jyyJEorlcItpgRkFfC849hZmDZ41me7L4,4799
|
|
50
|
+
asana_api_cli/cli/workspace_memberships.py,sha256=5JcFCA9KEgxlahQ2NCGLUfMymFAewg_zhGj7KQLNP2E,4580
|
|
51
|
+
asana_api_cli/cli/workspaces.py,sha256=vv5XhNEMQGXP4aNyzql5bZp_tRbybDp2i0RSmX4q3Z4,5982
|
|
52
|
+
asana_api_cli-1.2.0.dist-info/licenses/LICENSE,sha256=0x0LkhzzvBg1yYZvRGGOZ3ZZUVLmP4sK_jyUljmH4Pg,10764
|
|
53
|
+
asana_api_cli-1.2.0.dist-info/METADATA,sha256=0PcCgX_q6LdOOmHyHKzjmpUWP7QsTerCuz_OTDstjAE,3221
|
|
54
|
+
asana_api_cli-1.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
55
|
+
asana_api_cli-1.2.0.dist-info/entry_points.txt,sha256=dcNQUEQQh7ODXPh9TsCsj1jHycMgIJ7uFpz0Wwi1_UU,53
|
|
56
|
+
asana_api_cli-1.2.0.dist-info/top_level.txt,sha256=Hj5dHD-kV-G8pIp46m76N9spxBOwzxQ1Rh1ocdzoXw4,14
|
|
57
|
+
asana_api_cli-1.2.0.dist-info/RECORD,,
|