Skip to content

Commit ac48435

Browse files
author
Masanao Izumo
committed
Merge branch 'develop' for 3.3.0 release
2 parents cfaef05 + 9c136c0 commit ac48435

17 files changed

Lines changed: 1098 additions & 392 deletions

CHANGELOG.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
## [3.3.0] - 2026-06-29
11+
12+
### Added
13+
14+
- **`agents` and `ooo-entries` command groups**, picked up from the
15+
`python-asana` 5.2.5 release. `agents` reads a workspace's AI agents (Asana's
16+
AI Teammates): `get-agent`, `get-agents-for-workspace`. `ooo-entries` is full
17+
CRUD over a user's out-of-office entries: `create-ooo-entry`,
18+
`get-ooo-entries`, `get-ooo-entry`, `update-ooo-entry`, `delete-ooo-entry`.
19+
The command tree is built by introspecting the installed SDK, so these appear
20+
automatically when `python-asana >= 5.2.5` is installed.
21+
22+
### Fixed
23+
24+
- **`--set-default-header` given at more than one point in the command path no
25+
longer drops the earlier headers.** A header set before the command path (for
26+
example on the root) is now merged with one set after it, per header, with the
27+
later occurrence winning on a name collision — the same "the later occurrence
28+
wins when repeated" rule the other global options follow. Previously a second
29+
`--set-default-header` at a different level replaced the whole set, silently
30+
discarding the first.
31+
832
## [3.2.0] - 2026-06-14
933

1034
### Added
@@ -421,7 +445,8 @@ Combining a deprecated alias with its replacement (e.g.
421445

422446
- Initial release.
423447

424-
[Unreleased]: https://github.com/izumo-m/asana-api-cli/compare/v3.2.0...HEAD
448+
[Unreleased]: https://github.com/izumo-m/asana-api-cli/compare/v3.3.0...HEAD
449+
[3.3.0]: https://github.com/izumo-m/asana-api-cli/compare/v3.2.0...v3.3.0
425450
[3.2.0]: https://github.com/izumo-m/asana-api-cli/compare/v3.1.3...v3.2.0
426451
[3.1.3]: https://github.com/izumo-m/asana-api-cli/compare/v3.1.2...v3.1.3
427452
[3.1.2]: https://github.com/izumo-m/asana-api-cli/compare/v3.1.1...v3.1.2

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ automatically.
107107

108108
The token can be issued from the
109109
[Asana Developer Console](https://app.asana.com/0/developer-console).
110-
No token is needed for `--help` or argument validation errors.
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.)
111113

112114
```bash
113115
export ASANA_ACCESS_TOKEN="2/12345..."

docs/api-groups.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ CRUD coverage — SDK methods are the ground truth.
3333
| CLI group | Asana reference | Short description (`--help`) |
3434
| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------ |
3535
| `access-requests` | [Access Requests](https://developers.asana.com/reference/access-requests) | Manage private-object access requests |
36+
| `agents` | [Agents](https://developers.asana.com/reference/agents) | Read workspace AI agents (AI Teammates) |
3637
| `allocations` | [Allocations](https://developers.asana.com/reference/allocations) | Manage user allocations across projects |
3738
| `attachments` | [Attachments](https://developers.asana.com/reference/attachments) | Upload, list, and remove file attachments |
3839
| `audit-log-api` | [Audit Log API](https://developers.asana.com/reference/audit-log-api) | Read domain audit log events |
@@ -47,6 +48,7 @@ CRUD coverage — SDK methods are the ground truth.
4748
| `goals` | [Goals](https://developers.asana.com/reference/goals) | Manage organizational goals and metrics |
4849
| `jobs` | [Jobs](https://developers.asana.com/reference/jobs) | Check status of async background jobs |
4950
| `memberships` | [Memberships](https://developers.asana.com/reference/memberships) | Manage memberships across object types |
51+
| `ooo-entries` | [Out-of-Office Entries](https://developers.asana.com/reference/ooo-entries) | Manage out-of-office (OOO) entries |
5052
| `organization-exports` | [Organization Exports](https://developers.asana.com/reference/organization-exports) | Trigger and download org-wide exports |
5153
| `portfolio-memberships` | [Portfolio Memberships](https://developers.asana.com/reference/portfolio-memberships) | Read who has access to portfolios |
5254
| `portfolios` | [Portfolios](https://developers.asana.com/reference/portfolios) | Manage portfolios (project collections) |

docs/cli-sdk-mapping.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ In the **"SDK destination"** column:
5757
| `--key-file PATH` | `Configuration.key_file` | Direct property (client TLS key) |
5858
| `--assert-hostname / --no-assert-hostname` | `Configuration.assert_hostname` | Direct property; tri-state |
5959
| `--user-agent VALUE` | `ApiClient.user_agent` | Client setter: assigned on the `ApiClient` after construction in `AsanaSession.__init__` |
60-
| `--set-default-header NAME=VALUE` | `ApiClient.set_default_header(name, value)` | Client setter: repeatable, parsed by `structured_arg.default_header_callback`; each pair applied after construction. Session-wide (rides every request) and overrides per-call `--header-params` on a key collision (SDK merges defaults on top); **not redacted in `--debug`** (see [SECURITY.md](../SECURITY.md)) |
60+
| `--set-default-header NAME=VALUE` | `ApiClient.set_default_header(name, value)` | Client setter: repeatable, parsed by `structured_arg.default_header_callback`; each pair applied after construction. Session-wide (rides every request) and overrides per-call `--header-params` on a key collision (SDK merges defaults on top); in `--debug` **only `Authorization` / `Proxy-Authorization` values are redacted** — any other header name is logged verbatim (see [SECURITY.md](../SECURITY.md)) |
6161
| `--retry-strategy VALUE` | `Configuration.retry_strategy` | Struct member: parsed by `structured_arg` (`RETRY_FIELD_SCHEMA`), applied as `retry_strategy.new(**overrides)` so unspecified fields keep the SDK defaults |
6262
| `--connection-pool-maxsize N` | `Configuration.connection_pool_maxsize` | Direct property |
6363
| `--access-token TOKEN` | `Configuration.access_token` | Direct property; default source `$ASANA_ACCESS_TOKEN` |
@@ -83,7 +83,7 @@ forwarded straight to the SDK call as a kwarg — no `runtime` round-trip.
8383
|---|---|---|
8484
| `--item-limit N` | per-call kwarg `item_limit` | Forwarded by `_make_command` |
8585
| `--full-payload` | per-call kwarg `full_payload=True` | Forwarded by `_make_command` |
86-
| `--header-params VALUE` | per-call kwarg `header_params` | Parsed by `structured_arg`; **not redacted in `--debug`** (see SECURITY.md) |
86+
| `--header-params VALUE` | per-call kwarg `header_params` | Parsed by `structured_arg`; in `--debug` **only `Authorization` / `Proxy-Authorization` values are redacted** — any other header name is logged verbatim (see SECURITY.md) |
8787
| `--request-timeout SECONDS` | per-call kwarg `_request_timeout` | Forwarded by `_make_command`; propagated to every page request by the SDK `PageIterator` |
8888

8989
The `--limit` / `--offset` flags are docstring-derived (per-method) and appear

docs/development.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,29 @@ tools/
3838

3939
The CLI surface snapshot test (see
4040
[`architecture.md`](architecture.md#surface-snapshot-guardrail)) catches
41-
group/command/option churn introduced by an SDK bump. Procedure:
42-
43-
1. Edit `dependencies` in `pyproject.toml` to raise the lower bound.
44-
2. `uv sync` to install the new SDK.
45-
3. `uv run pytest` — failures in `test_cli_surface.py` print the diff.
41+
group/command/option churn introduced by an SDK bump. The `dependencies` lower
42+
bound in `pyproject.toml` is kept wide (`asana>=5.0.2,<6`) — the CLI adapts to
43+
whatever SDK is installed — so a bump tracks the *snapshot*, not the floor.
44+
Procedure:
45+
46+
1. `uv sync --upgrade-package asana` to relock and install the new SDK
47+
(`uv.lock` moves; `pyproject.toml` stays).
48+
2. Bump `_SNAPSHOT_ASANA_VERSION` to the new version in **both**
49+
`tests/test_cli_surface.py` and `tests/test_generate_python_snapshots.py`
50+
otherwise those snapshot guards silently *skip* on the new version instead
51+
of checking it.
52+
3. `uv run pytest` — failures in `test_cli_surface.py` /
53+
`test_generate_python_snapshots.py` print the diff.
4654
4. Review the diff; describe user-visible changes in `CHANGELOG.md`.
47-
5. Regenerate the fixture (exact command in `tests/test_cli_surface.py`'s module docstring).
55+
5. Regenerate the fixtures (exact command in each test's module docstring):
56+
- `tests/fixtures/cli_surface.json` — the CLI surface. Besides commands and
57+
options it pins each command's `paginatable` / `returns_iterator` /
58+
`does_upload` classification, so a new array-response or upload endpoint
59+
surfaces here as a fixture diff — there is no separate hand-maintained set.
60+
(`tests/test_sdk_boilerplate.py` independently proves those two classifiers
61+
still match the SDK source on the installed version.)
62+
- `tests/fixtures/generate_python/*.py` via
63+
`UPDATE_GENERATE_SNAPSHOTS=1 uv run pytest tests/test_generate_python_snapshots.py`.
4864
6. Verify Asana auth is still Bearer-token-only — confirm the new SDK still
4965
wires up only the token scheme:
5066

@@ -76,7 +92,10 @@ group/command/option churn introduced by an SDK bump. Procedure:
7692
[developers.asana.com/llms.txt](https://developers.asana.com/llms.txt)
7793
(an AI-friendly Markdown index of the reference) and/or the
7894
individual `/reference/<group>.md` pages.
79-
8. Commit `pyproject.toml`, `uv.lock`, `tests/fixtures/cli_surface.json`, and `CHANGELOG.md` together.
95+
8. Commit `uv.lock`, the two `_SNAPSHOT_ASANA_VERSION` bumps, the regenerated
96+
fixtures (`tests/fixtures/cli_surface.json` and any `generate_python/*`
97+
snapshots), and `CHANGELOG.md` together (plus any group-description edits
98+
from step 7).
8099

81100
## Trying shell completion locally
82101

docs/usage.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ asana-api workspaces get-workspaces
2323
asana-api --access-token "2/12345..." workspaces get-workspaces
2424
```
2525

26-
No token is needed for `--help` or argument-validation errors.
26+
No token is needed for `--help` or command-line parsing errors (unknown
27+
options, a missing required argument, a malformed `NAME=VALUE`). One exception:
28+
a `--query` / `--exception-query` jq filter is validated against the response
29+
payload, so a syntactically invalid filter surfaces only *after* the API
30+
call — which does need a token.
2731

2832
## Options
2933

@@ -369,7 +373,7 @@ Mirroring the SDK, `--proxy` is the only way to configure a proxy — the
369373

370374
### Credentials in the proxy URL are discarded
371375

372-
As of `python-asana` 5.2.4 — the latest version checked —
376+
As of `python-asana` 5.2.5 — the latest version checked —
373377
`--proxy http://user:pass@host:port` parses, but the credentials are **never
374378
sent**: the SDK stack (`python-asana` → urllib3) does not turn URL userinfo
375379
into a `Proxy-Authorization` header. urllib3's only built-in proxy-credential
@@ -420,7 +424,7 @@ like `--project` that the Asana API accepts in place of a workspace.
420424

421425
## File uploads
422426

423-
In `python-asana` 5.2.4 — the latest version checked, and most likely later
427+
In `python-asana` 5.2.5 — the latest version checked, and most likely later
424428
ones too — uploading a file whose name contains non-ASCII characters (accented
425429
letters, Japanese, emoji, …) stores a garbled (mojibake) filename on Asana.
426430
This is a long-standing bug in the SDK — see the

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "asana-api-cli"
3-
version = "3.2.0"
3+
version = "3.3.0"
44
description = "Command-line wrapper around the official Asana Python SDK"
55
authors = [{name = "Masanao Izumo"}]
66
readme = "README.md"

src/asana_api_cli/cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def _humanize_class_name(name: str) -> str:
224224
# a release blocker.
225225
_GROUP_DESCRIPTIONS: dict[str, str] = {
226226
"AccessRequests": "Manage private-object access requests",
227+
"Agents": "Read workspace AI agents (AI Teammates)",
227228
"Allocations": "Manage user allocations across projects",
228229
"Attachments": "Upload, list, and remove file attachments",
229230
"AuditLogAPI": "Read domain audit log events",
@@ -238,6 +239,7 @@ def _humanize_class_name(name: str) -> str:
238239
"Goals": "Manage organizational goals and metrics",
239240
"Jobs": "Check status of async background jobs",
240241
"Memberships": "Manage memberships across object types",
242+
"OooEntries": "Manage out-of-office (OOO) entries",
241243
"OrganizationExports": "Trigger and download org-wide exports",
242244
"PortfolioMemberships": "Read who has access to portfolios",
243245
"Portfolios": "Manage portfolios (project collections)",
@@ -1383,6 +1385,16 @@ def introspect_to_manifest() -> dict[str, Any]:
13831385
"positional": list(op.positional),
13841386
"has_opts": op.has_opts,
13851387
"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,
13861398
"params": [
13871399
{
13881400
"name": p.name,

src/asana_api_cli/click_ext.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
# code path is naturally dead when the flag is hidden. We instantiate
4646
# ``Configuration()`` because ``retry_strategy`` is an instance attribute
4747
# set in ``__init__`` — checking ``hasattr`` on the class itself returns
48-
# False even on 5.2.4.
48+
# False even on 5.2.5.
4949
_SDK_HAS_RETRY_STRATEGY: bool = hasattr(asana.Configuration(), "retry_strategy")
5050

5151

@@ -342,12 +342,22 @@ def _apply_global_to_runtime(name: str, value: Any) -> None:
342342
option declarations and the dataclass fields are authored together, and the
343343
inventory tests in ``test_click_ext.py`` (``TestGlobalOptionNamesInventory``)
344344
pin the ``GLOBAL_OPTION_NAMES`` set against ``_Runtime``'s fields — so a
345-
direct ``setattr`` suffices. Every option, ``access_token`` included,
346-
follows the same last-wins rule: the command-line value from the deepest
347-
level reached overwrites whatever an earlier level wrote. An explicit empty
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
349349
``AsanaSession.from_env`` then falls back to ``$ASANA_ACCESS_TOKEN``.
350350
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+
351361
The caller, :func:`_consume_global_options`, only ever passes names drawn
352362
from ``GLOBAL_OPTION_NAMES`` and only when the parameter source is
353363
``ParameterSource.COMMANDLINE``. This is the single application path for
@@ -356,6 +366,9 @@ def _apply_global_to_runtime(name: str, value: Any) -> None:
356366
tri-state toggles' (``verify_ssl`` / ``assert_hostname`` /
357367
``return_page_iterator``) ``None`` default never reaches here.
358368
"""
369+
if name == "default_headers":
370+
runtime.default_headers = {**(runtime.default_headers or {}), **value}
371+
return
359372
setattr(runtime, name, value)
360373

361374

src/asana_api_cli/multibyte_filename.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class MultibyteFilenameSupport:
1717
"""Make multipart uploads round-trip filenames with non-ASCII characters.
1818
19-
In ``python-asana`` 5.2.4 (the latest version checked, and likely later
19+
In ``python-asana`` 5.2.5 (the latest version checked, and likely later
2020
ones too), uploading a file whose name has characters outside ASCII
2121
stores a garbled (mojibake) name on Asana: the SDK's multipart encoder
2222
emits only ``filename="..."`` and omits the RFC 5987 ``filename*=``

0 commit comments

Comments
 (0)