asana-api-cli 1.3.0__tar.gz → 1.5.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-1.3.0/src/asana_api_cli.egg-info → asana_api_cli-1.5.0}/PKG-INFO +68 -11
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/README.md +66 -10
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/pyproject.toml +3 -1
- asana_api_cli-1.5.0/src/asana_api_cli/cli/__init__.py +93 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/access_requests.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/allocations.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/attachments.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/audit_log_api.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/batch_api.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/budgets.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/custom_field_settings.py +50 -19
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/custom_fields.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/custom_types.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/events.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/exports.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/goal_relationships.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/goals.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/jobs.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/memberships.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/organization_exports.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/portfolio_memberships.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/portfolios.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/project_briefs.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/project_memberships.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/project_portfolio_settings.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/project_statuses.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/project_templates.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/projects.py +66 -25
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/rates.py +18 -7
- asana_api_cli-1.5.0/src/asana_api_cli/cli/reactions.py +45 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/roles.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/rules.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/sections.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/status_updates.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/stories.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/tags.py +50 -19
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/task_templates.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/tasks.py +130 -49
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/team_memberships.py +50 -19
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/teams.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/time_periods.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/time_tracking_categories.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/time_tracking_entries.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/timesheet_approval_statuses.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/typeahead.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/user_task_lists.py +2 -1
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/users.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/webhooks.py +18 -7
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/workspace_memberships.py +34 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/cli/workspaces.py +18 -7
- asana_api_cli-1.5.0/src/asana_api_cli/click_ext.py +276 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/session.py +63 -14
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/version.py +1 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0/src/asana_api_cli.egg-info}/PKG-INFO +68 -11
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli.egg-info/SOURCES.txt +3 -0
- asana_api_cli-1.5.0/tests/test_click_ext.py +176 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/tests/test_codegen.py +58 -13
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/tests/test_formatter.py +6 -13
- asana_api_cli-1.5.0/tests/test_py310_compat.py +41 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/tests/test_session.py +2 -0
- asana_api_cli-1.3.0/src/asana_api_cli/cli/__init__.py +0 -140
- asana_api_cli-1.3.0/src/asana_api_cli/cli/reactions.py +0 -34
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/LICENSE +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/setup.cfg +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/__init__.py +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli/formatter.py +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli.egg-info/dependency_links.txt +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli.egg-info/entry_points.txt +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli.egg-info/requires.txt +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/src/asana_api_cli.egg-info/top_level.txt +0 -0
- {asana_api_cli-1.3.0 → asana_api_cli-1.5.0}/tests/test_version.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asana-api-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Command-line wrapper around the official Asana Python SDK
|
|
5
5
|
Author-email: Masanao Izumo <asana@masanao.site>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/izumo-m/asana-api-cli
|
|
8
8
|
Project-URL: Repository, https://github.com/izumo-m/asana-api-cli
|
|
9
9
|
Project-URL: Issues, https://github.com/izumo-m/asana-api-cli/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/izumo-m/asana-api-cli/blob/main/CHANGELOG.md
|
|
10
11
|
Requires-Python: >=3.10
|
|
11
12
|
Description-Content-Type: text/markdown
|
|
12
13
|
License-File: LICENSE
|
|
@@ -65,28 +66,27 @@ and add the line to `~/.zshrc` or `~/.config/fish/config.fish` respectively.
|
|
|
65
66
|
## Usage
|
|
66
67
|
|
|
67
68
|
```bash
|
|
68
|
-
#
|
|
69
|
+
# Version and help
|
|
69
70
|
asana-api --version
|
|
70
|
-
|
|
71
|
-
# List commands
|
|
72
71
|
asana-api --help
|
|
73
72
|
asana-api tasks --help
|
|
74
73
|
asana-api tasks get-tasks --help
|
|
75
74
|
|
|
76
|
-
# List workspaces
|
|
75
|
+
# List workspaces and projects
|
|
77
76
|
asana-api workspaces get-workspaces
|
|
78
|
-
|
|
79
|
-
# List projects (workspace resolved from ASANA_DEFAULT_WORKSPACE)
|
|
80
77
|
asana-api projects get-projects-for-workspace
|
|
81
78
|
asana-api projects get-projects --workspace <WORKSPACE_GID>
|
|
82
79
|
|
|
83
|
-
# List tasks (first page)
|
|
80
|
+
# List tasks (first page only by default)
|
|
84
81
|
asana-api tasks get-tasks --project <PROJECT_GID>
|
|
85
82
|
|
|
86
|
-
#
|
|
87
|
-
asana-api tasks get-tasks --project <PROJECT_GID> --
|
|
83
|
+
# Preview the first few items
|
|
84
|
+
asana-api tasks get-tasks --project <PROJECT_GID> --max-items 5
|
|
85
|
+
|
|
86
|
+
# Fetch every item across pages
|
|
87
|
+
asana-api tasks get-tasks --project <PROJECT_GID> --all-items
|
|
88
88
|
|
|
89
|
-
# Single task
|
|
89
|
+
# Single task
|
|
90
90
|
asana-api tasks get-task --task <TASK_GID>
|
|
91
91
|
|
|
92
92
|
# Create a task (body is a JSON string)
|
|
@@ -97,6 +97,9 @@ asana-api tasks get-tasks --project <PID> --output table
|
|
|
97
97
|
asana-api tasks get-tasks --project <PID> --query '.data' --output csv
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
See [Pagination](#pagination) for fetching across pages and
|
|
101
|
+
[Global options](#global-options) for `--debug`, `--access-token`, etc.
|
|
102
|
+
|
|
100
103
|
### Workspace resolution
|
|
101
104
|
|
|
102
105
|
Many API endpoints require a workspace. For those endpoints (e.g.
|
|
@@ -110,6 +113,60 @@ fallback is **not** used — pass `--workspace` explicitly if needed. This
|
|
|
110
113
|
prevents conflicts with other scope parameters like `--project` that are
|
|
111
114
|
mutually exclusive with workspace in the Asana API.
|
|
112
115
|
|
|
116
|
+
## Pagination
|
|
117
|
+
|
|
118
|
+
Listing endpoints (e.g. `tasks get-tasks`) return paginated results. The CLI
|
|
119
|
+
provides four ways to control how much is fetched:
|
|
120
|
+
|
|
121
|
+
| Option | Behavior |
|
|
122
|
+
|--------|----------|
|
|
123
|
+
| (none) | Fetch a single page (Asana default: 100 items) |
|
|
124
|
+
| `--max-items N` | Fetch up to N items, auto-paginating across pages. The last request is automatically capped to the remaining count. |
|
|
125
|
+
| `--all-items` | Fetch every page until the server reports no more |
|
|
126
|
+
| `--offset <TOKEN>` | Manual pagination: pass the `next_page.offset` token from the previous response |
|
|
127
|
+
|
|
128
|
+
`--max-items` and `--all-items` are mutually exclusive.
|
|
129
|
+
|
|
130
|
+
`--page-size N` tunes the per-page request size (Asana API requires 1-100,
|
|
131
|
+
default 100). Rarely needed — combine with `--all-items` or `--max-items` only
|
|
132
|
+
when the default doesn't suit (e.g. very large rows or rate-limit tuning).
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Auto-paginate up to 250 items
|
|
136
|
+
asana-api tasks get-tasks --project <PID> --max-items 250
|
|
137
|
+
|
|
138
|
+
# Fetch everything
|
|
139
|
+
asana-api tasks get-tasks --project <PID> --all-items
|
|
140
|
+
|
|
141
|
+
# Manual pagination using the offset token
|
|
142
|
+
asana-api tasks get-tasks --project <PID> --offset <TOKEN>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Global options
|
|
146
|
+
|
|
147
|
+
These options work at any level of the command tree, so the following are
|
|
148
|
+
equivalent:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
asana-api --debug tasks get-tasks --project <PID>
|
|
152
|
+
asana-api tasks get-tasks --project <PID> --debug
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When the same option is given at multiple levels, the more specific (later)
|
|
156
|
+
one wins.
|
|
157
|
+
|
|
158
|
+
| Option | Description |
|
|
159
|
+
|--------|-------------|
|
|
160
|
+
| `--access-token TOKEN` | Asana personal access token (default: `$ASANA_ACCESS_TOKEN`) |
|
|
161
|
+
| `--host URL` | Override API base URL (default: `https://app.asana.com/api/1.0`) |
|
|
162
|
+
| `--proxy URL` | HTTP/HTTPS proxy URL |
|
|
163
|
+
| `--no-verify-ssl` | Disable TLS certificate verification (insecure) |
|
|
164
|
+
| `--ca-cert PATH` | Path to a PEM bundle of trusted CA certificates |
|
|
165
|
+
| `--retries N` | Number of retries on 429/5xx responses (default: 5) |
|
|
166
|
+
| `--timeout SECONDS` | Per-request timeout in seconds |
|
|
167
|
+
| `--temp-dir PATH` | Directory for temporary downloads |
|
|
168
|
+
| `--debug` | Print HTTP request/response to stderr for troubleshooting |
|
|
169
|
+
|
|
113
170
|
## Development
|
|
114
171
|
|
|
115
172
|
See [docs/development.md](https://github.com/izumo-m/asana-api-cli/blob/main/docs/development.md)
|
|
@@ -47,28 +47,27 @@ and add the line to `~/.zshrc` or `~/.config/fish/config.fish` respectively.
|
|
|
47
47
|
## Usage
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
#
|
|
50
|
+
# Version and help
|
|
51
51
|
asana-api --version
|
|
52
|
-
|
|
53
|
-
# List commands
|
|
54
52
|
asana-api --help
|
|
55
53
|
asana-api tasks --help
|
|
56
54
|
asana-api tasks get-tasks --help
|
|
57
55
|
|
|
58
|
-
# List workspaces
|
|
56
|
+
# List workspaces and projects
|
|
59
57
|
asana-api workspaces get-workspaces
|
|
60
|
-
|
|
61
|
-
# List projects (workspace resolved from ASANA_DEFAULT_WORKSPACE)
|
|
62
58
|
asana-api projects get-projects-for-workspace
|
|
63
59
|
asana-api projects get-projects --workspace <WORKSPACE_GID>
|
|
64
60
|
|
|
65
|
-
# List tasks (first page)
|
|
61
|
+
# List tasks (first page only by default)
|
|
66
62
|
asana-api tasks get-tasks --project <PROJECT_GID>
|
|
67
63
|
|
|
68
|
-
#
|
|
69
|
-
asana-api tasks get-tasks --project <PROJECT_GID> --
|
|
64
|
+
# Preview the first few items
|
|
65
|
+
asana-api tasks get-tasks --project <PROJECT_GID> --max-items 5
|
|
66
|
+
|
|
67
|
+
# Fetch every item across pages
|
|
68
|
+
asana-api tasks get-tasks --project <PROJECT_GID> --all-items
|
|
70
69
|
|
|
71
|
-
# Single task
|
|
70
|
+
# Single task
|
|
72
71
|
asana-api tasks get-task --task <TASK_GID>
|
|
73
72
|
|
|
74
73
|
# Create a task (body is a JSON string)
|
|
@@ -79,6 +78,9 @@ asana-api tasks get-tasks --project <PID> --output table
|
|
|
79
78
|
asana-api tasks get-tasks --project <PID> --query '.data' --output csv
|
|
80
79
|
```
|
|
81
80
|
|
|
81
|
+
See [Pagination](#pagination) for fetching across pages and
|
|
82
|
+
[Global options](#global-options) for `--debug`, `--access-token`, etc.
|
|
83
|
+
|
|
82
84
|
### Workspace resolution
|
|
83
85
|
|
|
84
86
|
Many API endpoints require a workspace. For those endpoints (e.g.
|
|
@@ -92,6 +94,60 @@ fallback is **not** used — pass `--workspace` explicitly if needed. This
|
|
|
92
94
|
prevents conflicts with other scope parameters like `--project` that are
|
|
93
95
|
mutually exclusive with workspace in the Asana API.
|
|
94
96
|
|
|
97
|
+
## Pagination
|
|
98
|
+
|
|
99
|
+
Listing endpoints (e.g. `tasks get-tasks`) return paginated results. The CLI
|
|
100
|
+
provides four ways to control how much is fetched:
|
|
101
|
+
|
|
102
|
+
| Option | Behavior |
|
|
103
|
+
|--------|----------|
|
|
104
|
+
| (none) | Fetch a single page (Asana default: 100 items) |
|
|
105
|
+
| `--max-items N` | Fetch up to N items, auto-paginating across pages. The last request is automatically capped to the remaining count. |
|
|
106
|
+
| `--all-items` | Fetch every page until the server reports no more |
|
|
107
|
+
| `--offset <TOKEN>` | Manual pagination: pass the `next_page.offset` token from the previous response |
|
|
108
|
+
|
|
109
|
+
`--max-items` and `--all-items` are mutually exclusive.
|
|
110
|
+
|
|
111
|
+
`--page-size N` tunes the per-page request size (Asana API requires 1-100,
|
|
112
|
+
default 100). Rarely needed — combine with `--all-items` or `--max-items` only
|
|
113
|
+
when the default doesn't suit (e.g. very large rows or rate-limit tuning).
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Auto-paginate up to 250 items
|
|
117
|
+
asana-api tasks get-tasks --project <PID> --max-items 250
|
|
118
|
+
|
|
119
|
+
# Fetch everything
|
|
120
|
+
asana-api tasks get-tasks --project <PID> --all-items
|
|
121
|
+
|
|
122
|
+
# Manual pagination using the offset token
|
|
123
|
+
asana-api tasks get-tasks --project <PID> --offset <TOKEN>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Global options
|
|
127
|
+
|
|
128
|
+
These options work at any level of the command tree, so the following are
|
|
129
|
+
equivalent:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
asana-api --debug tasks get-tasks --project <PID>
|
|
133
|
+
asana-api tasks get-tasks --project <PID> --debug
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
When the same option is given at multiple levels, the more specific (later)
|
|
137
|
+
one wins.
|
|
138
|
+
|
|
139
|
+
| Option | Description |
|
|
140
|
+
|--------|-------------|
|
|
141
|
+
| `--access-token TOKEN` | Asana personal access token (default: `$ASANA_ACCESS_TOKEN`) |
|
|
142
|
+
| `--host URL` | Override API base URL (default: `https://app.asana.com/api/1.0`) |
|
|
143
|
+
| `--proxy URL` | HTTP/HTTPS proxy URL |
|
|
144
|
+
| `--no-verify-ssl` | Disable TLS certificate verification (insecure) |
|
|
145
|
+
| `--ca-cert PATH` | Path to a PEM bundle of trusted CA certificates |
|
|
146
|
+
| `--retries N` | Number of retries on 429/5xx responses (default: 5) |
|
|
147
|
+
| `--timeout SECONDS` | Per-request timeout in seconds |
|
|
148
|
+
| `--temp-dir PATH` | Directory for temporary downloads |
|
|
149
|
+
| `--debug` | Print HTTP request/response to stderr for troubleshooting |
|
|
150
|
+
|
|
95
151
|
## Development
|
|
96
152
|
|
|
97
153
|
See [docs/development.md](https://github.com/izumo-m/asana-api-cli/blob/main/docs/development.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "asana-api-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
4
4
|
description = "Command-line wrapper around the official Asana Python SDK"
|
|
5
5
|
authors = [{name = "Masanao Izumo", email = "asana@masanao.site"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -17,6 +17,7 @@ dependencies = [
|
|
|
17
17
|
Homepage = "https://github.com/izumo-m/asana-api-cli"
|
|
18
18
|
Repository = "https://github.com/izumo-m/asana-api-cli"
|
|
19
19
|
Issues = "https://github.com/izumo-m/asana-api-cli/issues"
|
|
20
|
+
Changelog = "https://github.com/izumo-m/asana-api-cli/blob/main/CHANGELOG.md"
|
|
20
21
|
|
|
21
22
|
[project.scripts]
|
|
22
23
|
asana-api = "asana_api_cli.cli:main"
|
|
@@ -27,6 +28,7 @@ dev = [
|
|
|
27
28
|
"pytest>=9,<10",
|
|
28
29
|
"build>=1,<2",
|
|
29
30
|
"twine>=6,<7",
|
|
31
|
+
"vermin>=1.6,<2",
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
[build-system]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# This file is auto-generated by tools/codegen.py — do not edit manually.
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from asana_api_cli.click_ext import LazyGroup
|
|
7
|
+
from asana_api_cli.session import runtime
|
|
8
|
+
from asana_api_cli.version import version_string
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
LAZY_SUBCOMMANDS: dict[str, tuple[str, str]] = {
|
|
12
|
+
"access-requests": ("asana_api_cli.cli.access_requests:access_requests_group", "AccessRequests commands."),
|
|
13
|
+
"allocations": ("asana_api_cli.cli.allocations:allocations_group", "Allocations commands."),
|
|
14
|
+
"attachments": ("asana_api_cli.cli.attachments:attachments_group", "Attachments commands."),
|
|
15
|
+
"audit-log-api": ("asana_api_cli.cli.audit_log_api:audit_log_api_group", "AuditLogAPI commands."),
|
|
16
|
+
"batch-api": ("asana_api_cli.cli.batch_api:batch_api_group", "BatchAPI commands."),
|
|
17
|
+
"budgets": ("asana_api_cli.cli.budgets:budgets_group", "Budgets commands."),
|
|
18
|
+
"custom-field-settings": ("asana_api_cli.cli.custom_field_settings:custom_field_settings_group", "CustomFieldSettings commands."),
|
|
19
|
+
"custom-fields": ("asana_api_cli.cli.custom_fields:custom_fields_group", "CustomFields commands."),
|
|
20
|
+
"custom-types": ("asana_api_cli.cli.custom_types:custom_types_group", "CustomTypes commands."),
|
|
21
|
+
"events": ("asana_api_cli.cli.events:events_group", "Events commands."),
|
|
22
|
+
"exports": ("asana_api_cli.cli.exports:exports_group", "Exports commands."),
|
|
23
|
+
"goal-relationships": ("asana_api_cli.cli.goal_relationships:goal_relationships_group", "GoalRelationships commands."),
|
|
24
|
+
"goals": ("asana_api_cli.cli.goals:goals_group", "Goals commands."),
|
|
25
|
+
"jobs": ("asana_api_cli.cli.jobs:jobs_group", "Jobs commands."),
|
|
26
|
+
"memberships": ("asana_api_cli.cli.memberships:memberships_group", "Memberships commands."),
|
|
27
|
+
"organization-exports": ("asana_api_cli.cli.organization_exports:organization_exports_group", "OrganizationExports commands."),
|
|
28
|
+
"portfolio-memberships": ("asana_api_cli.cli.portfolio_memberships:portfolio_memberships_group", "PortfolioMemberships commands."),
|
|
29
|
+
"portfolios": ("asana_api_cli.cli.portfolios:portfolios_group", "Portfolios commands."),
|
|
30
|
+
"project-briefs": ("asana_api_cli.cli.project_briefs:project_briefs_group", "ProjectBriefs commands."),
|
|
31
|
+
"project-memberships": ("asana_api_cli.cli.project_memberships:project_memberships_group", "ProjectMemberships commands."),
|
|
32
|
+
"project-portfolio-settings": ("asana_api_cli.cli.project_portfolio_settings:project_portfolio_settings_group", "ProjectPortfolioSettings commands."),
|
|
33
|
+
"project-statuses": ("asana_api_cli.cli.project_statuses:project_statuses_group", "ProjectStatuses commands."),
|
|
34
|
+
"project-templates": ("asana_api_cli.cli.project_templates:project_templates_group", "ProjectTemplates commands."),
|
|
35
|
+
"projects": ("asana_api_cli.cli.projects:projects_group", "Projects commands."),
|
|
36
|
+
"rates": ("asana_api_cli.cli.rates:rates_group", "Rates commands."),
|
|
37
|
+
"reactions": ("asana_api_cli.cli.reactions:reactions_group", "Reactions commands."),
|
|
38
|
+
"roles": ("asana_api_cli.cli.roles:roles_group", "Roles commands."),
|
|
39
|
+
"rules": ("asana_api_cli.cli.rules:rules_group", "Rules commands."),
|
|
40
|
+
"sections": ("asana_api_cli.cli.sections:sections_group", "Sections commands."),
|
|
41
|
+
"status-updates": ("asana_api_cli.cli.status_updates:status_updates_group", "StatusUpdates commands."),
|
|
42
|
+
"stories": ("asana_api_cli.cli.stories:stories_group", "Stories commands."),
|
|
43
|
+
"tags": ("asana_api_cli.cli.tags:tags_group", "Tags commands."),
|
|
44
|
+
"task-templates": ("asana_api_cli.cli.task_templates:task_templates_group", "TaskTemplates commands."),
|
|
45
|
+
"tasks": ("asana_api_cli.cli.tasks:tasks_group", "Tasks commands."),
|
|
46
|
+
"team-memberships": ("asana_api_cli.cli.team_memberships:team_memberships_group", "TeamMemberships commands."),
|
|
47
|
+
"teams": ("asana_api_cli.cli.teams:teams_group", "Teams commands."),
|
|
48
|
+
"time-periods": ("asana_api_cli.cli.time_periods:time_periods_group", "TimePeriods commands."),
|
|
49
|
+
"time-tracking-categories": ("asana_api_cli.cli.time_tracking_categories:time_tracking_categories_group", "TimeTrackingCategories commands."),
|
|
50
|
+
"time-tracking-entries": ("asana_api_cli.cli.time_tracking_entries:time_tracking_entries_group", "TimeTrackingEntries commands."),
|
|
51
|
+
"timesheet-approval-statuses": ("asana_api_cli.cli.timesheet_approval_statuses:timesheet_approval_statuses_group", "TimesheetApprovalStatuses commands."),
|
|
52
|
+
"typeahead": ("asana_api_cli.cli.typeahead:typeahead_group", "Typeahead commands."),
|
|
53
|
+
"user-task-lists": ("asana_api_cli.cli.user_task_lists:user_task_lists_group", "UserTaskLists commands."),
|
|
54
|
+
"users": ("asana_api_cli.cli.users:users_group", "Users commands."),
|
|
55
|
+
"webhooks": ("asana_api_cli.cli.webhooks:webhooks_group", "Webhooks commands."),
|
|
56
|
+
"workspace-memberships": ("asana_api_cli.cli.workspace_memberships:workspace_memberships_group", "WorkspaceMemberships commands."),
|
|
57
|
+
"workspaces": ("asana_api_cli.cli.workspaces:workspaces_group", "Workspaces commands."),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.group(cls=LazyGroup, lazy_subcommands=LAZY_SUBCOMMANDS)
|
|
62
|
+
@click.version_option(version_string(), prog_name="asana-api")
|
|
63
|
+
@click.option("--host", default=None, help="Override API base URL (default: https://app.asana.com/api/1.0)")
|
|
64
|
+
@click.option("--proxy", default=None, help="HTTP/HTTPS proxy URL")
|
|
65
|
+
@click.option("--no-verify-ssl", is_flag=True, default=False, help="Disable TLS certificate verification (insecure)")
|
|
66
|
+
@click.option("--ca-cert", "ca_cert", default=None, type=click.Path(exists=True, dir_okay=False), help="Path to a PEM bundle of trusted CA certificates")
|
|
67
|
+
@click.option("--retries", type=int, default=None, help="Number of retries on 429/5xx responses (default: 5)")
|
|
68
|
+
@click.option("--timeout", type=float, default=None, help="Per-request timeout in seconds")
|
|
69
|
+
@click.option("--access-token", "access_token", default=None, help="Asana personal access token (default: $ASANA_ACCESS_TOKEN)")
|
|
70
|
+
@click.option("--temp-dir", "temp_dir", default=None, type=click.Path(file_okay=False), help="Directory for temporary downloads")
|
|
71
|
+
@click.option("--debug", is_flag=True, default=False, help="Print HTTP request/response to stderr for troubleshooting")
|
|
72
|
+
def main(
|
|
73
|
+
host: str | None,
|
|
74
|
+
proxy: str | None,
|
|
75
|
+
no_verify_ssl: bool,
|
|
76
|
+
ca_cert: str | None,
|
|
77
|
+
retries: int | None,
|
|
78
|
+
timeout: float | None,
|
|
79
|
+
access_token: str | None,
|
|
80
|
+
temp_dir: str | None,
|
|
81
|
+
debug: bool,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Asana API CLI (SDK-backed wrapper)."""
|
|
84
|
+
runtime.host = host
|
|
85
|
+
runtime.proxy = proxy
|
|
86
|
+
runtime.verify_ssl = not no_verify_ssl
|
|
87
|
+
runtime.ssl_ca_cert = ca_cert
|
|
88
|
+
runtime.retries = retries
|
|
89
|
+
runtime.timeout = timeout
|
|
90
|
+
if access_token:
|
|
91
|
+
runtime.access_token = access_token
|
|
92
|
+
runtime.temp_dir = temp_dir
|
|
93
|
+
runtime.debug = debug
|
|
@@ -6,11 +6,12 @@ from typing import Any
|
|
|
6
6
|
import click
|
|
7
7
|
from asana import AccessRequestsApi
|
|
8
8
|
|
|
9
|
+
from asana_api_cli.click_ext import GroupWithGlobalOptions
|
|
9
10
|
from asana_api_cli.formatter import formatted
|
|
10
11
|
from asana_api_cli.session import AsanaSession, resolve_body, resolve_workspace
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.group("access-requests")
|
|
14
|
+
@click.group("access-requests", cls=GroupWithGlobalOptions)
|
|
14
15
|
def access_requests_group() -> None:
|
|
15
16
|
"""AccessRequests commands."""
|
|
16
17
|
|
|
@@ -6,11 +6,12 @@ from typing import Any
|
|
|
6
6
|
import click
|
|
7
7
|
from asana import AllocationsApi
|
|
8
8
|
|
|
9
|
+
from asana_api_cli.click_ext import GroupWithGlobalOptions
|
|
9
10
|
from asana_api_cli.formatter import formatted
|
|
10
11
|
from asana_api_cli.session import AsanaSession, resolve_body, resolve_workspace
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.group("allocations")
|
|
14
|
+
@click.group("allocations", cls=GroupWithGlobalOptions)
|
|
14
15
|
def allocations_group() -> None:
|
|
15
16
|
"""Allocations commands."""
|
|
16
17
|
|
|
@@ -58,22 +59,30 @@ def get_allocation(allocation: str, opt_fields: str | None) -> Any:
|
|
|
58
59
|
@allocations_group.command("get-allocations")
|
|
59
60
|
@click.option("--workspace", default=None, help="Workspace GID (falls back to ASANA_DEFAULT_WORKSPACE)")
|
|
60
61
|
@click.option("--assignee", default=None, help="Globally unique identifier for the user or placeholder the allocation is assigned to.")
|
|
61
|
-
@click.option("--limit", type=int, default=None, help="Results per page. The number of objects to return per page. The value must be between 1 and 100.")
|
|
62
62
|
@click.option("--offset", default=None, help="Offset token. An offset to the next page returned by the API. A pagination request will return an offset token, which can be used as an input parameter to the next request. If an offset is not pass...")
|
|
63
63
|
@click.option("--opt-fields", default=None, help="This endpoint returns a resource which excludes some properties by default. To include those optional properties, set this query parameter to a comma-separated list of the properties you wish to in...")
|
|
64
64
|
@click.option("--parent", default=None, help="Globally unique identifier for the project to filter allocations by.")
|
|
65
|
-
@click.option("--
|
|
65
|
+
@click.option("--all-items", "all_items", is_flag=True, default=False, help="Fetch all items (no cap)")
|
|
66
|
+
@click.option("--paginate", "paginate", is_flag=True, default=False, help="(Deprecated) Alias for --all-items")
|
|
67
|
+
@click.option("--page-size", "page_size", type=int, default=None, help="Items per page (Asana API requires 1-100, default 100)")
|
|
68
|
+
@click.option("--max-items", "max_items", type=int, default=None, help="Stop after fetching this many items in total")
|
|
66
69
|
@formatted
|
|
67
|
-
def get_allocations(workspace: str | None, assignee: str | None,
|
|
70
|
+
def get_allocations(workspace: str | None, assignee: str | None, offset: str | None, opt_fields: str | None, parent: str | None, all_items: bool, paginate: bool, page_size: int | None, max_items: int | None) -> Any:
|
|
68
71
|
"""Get multiple allocations"""
|
|
69
72
|
resolved_workspace = resolve_workspace(workspace, required=False)
|
|
70
|
-
|
|
73
|
+
if paginate:
|
|
74
|
+
click.echo("Warning: --paginate is deprecated; use --all-items instead.", err=True)
|
|
75
|
+
fetch_all = all_items or paginate
|
|
76
|
+
if fetch_all and max_items is not None:
|
|
77
|
+
raise click.UsageError("--max-items cannot be combined with --all-items (or its deprecated alias --paginate)")
|
|
78
|
+
effective_page_size = page_size
|
|
79
|
+
if max_items is not None and (page_size is None or page_size > max_items):
|
|
80
|
+
effective_page_size = max_items
|
|
81
|
+
session = AsanaSession.from_env(paginate=fetch_all, page_size=effective_page_size)
|
|
71
82
|
api = AllocationsApi(session.client)
|
|
72
83
|
opts: dict[str, Any] = {}
|
|
73
84
|
if assignee is not None:
|
|
74
85
|
opts["assignee"] = assignee
|
|
75
|
-
if limit is not None:
|
|
76
|
-
opts["limit"] = limit
|
|
77
86
|
if offset is not None:
|
|
78
87
|
opts["offset"] = offset
|
|
79
88
|
if opt_fields is not None:
|
|
@@ -82,6 +91,8 @@ def get_allocations(workspace: str | None, assignee: str | None, limit: int | No
|
|
|
82
91
|
opts["parent"] = parent
|
|
83
92
|
if resolved_workspace is not None:
|
|
84
93
|
opts["workspace"] = resolved_workspace
|
|
94
|
+
if max_items is not None:
|
|
95
|
+
return session.fetch_capped(api.get_allocations, opts=opts, max_items=max_items)
|
|
85
96
|
return api.get_allocations(opts)
|
|
86
97
|
|
|
87
98
|
|
|
@@ -6,11 +6,12 @@ from typing import Any
|
|
|
6
6
|
import click
|
|
7
7
|
from asana import AttachmentsApi
|
|
8
8
|
|
|
9
|
+
from asana_api_cli.click_ext import GroupWithGlobalOptions
|
|
9
10
|
from asana_api_cli.formatter import formatted
|
|
10
11
|
from asana_api_cli.session import AsanaSession, resolve_body, resolve_workspace
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.group("attachments")
|
|
14
|
+
@click.group("attachments", cls=GroupWithGlobalOptions)
|
|
14
15
|
def attachments_group() -> None:
|
|
15
16
|
"""Attachments commands."""
|
|
16
17
|
|
|
@@ -73,20 +74,30 @@ def get_attachment(attachment: str, opt_fields: str | None) -> Any:
|
|
|
73
74
|
|
|
74
75
|
@attachments_group.command("get-attachments-for-object")
|
|
75
76
|
@click.option("--parent", required=True, help="Globally unique identifier for object to fetch statuses from. Must be a GID for a `project`, `project_brief`, or `task`.")
|
|
76
|
-
@click.option("--limit", type=int, default=None, help="Results per page. The number of objects to return per page. The value must be between 1 and 100.")
|
|
77
77
|
@click.option("--offset", default=None, help="Offset token. An offset to the next page returned by the API. A pagination request will return an offset token, which can be used as an input parameter to the next request. If an offset is not pass...")
|
|
78
78
|
@click.option("--opt-fields", default=None, help="This endpoint returns a resource which excludes some properties by default. To include those optional properties, set this query parameter to a comma-separated list of the properties you wish to in...")
|
|
79
|
-
@click.option("--
|
|
79
|
+
@click.option("--all-items", "all_items", is_flag=True, default=False, help="Fetch all items (no cap)")
|
|
80
|
+
@click.option("--paginate", "paginate", is_flag=True, default=False, help="(Deprecated) Alias for --all-items")
|
|
81
|
+
@click.option("--page-size", "page_size", type=int, default=None, help="Items per page (Asana API requires 1-100, default 100)")
|
|
82
|
+
@click.option("--max-items", "max_items", type=int, default=None, help="Stop after fetching this many items in total")
|
|
80
83
|
@formatted
|
|
81
|
-
def get_attachments_for_object(parent: str,
|
|
84
|
+
def get_attachments_for_object(parent: str, offset: str | None, opt_fields: str | None, all_items: bool, paginate: bool, page_size: int | None, max_items: int | None) -> Any:
|
|
82
85
|
"""Get attachments from an object"""
|
|
83
|
-
|
|
86
|
+
if paginate:
|
|
87
|
+
click.echo("Warning: --paginate is deprecated; use --all-items instead.", err=True)
|
|
88
|
+
fetch_all = all_items or paginate
|
|
89
|
+
if fetch_all and max_items is not None:
|
|
90
|
+
raise click.UsageError("--max-items cannot be combined with --all-items (or its deprecated alias --paginate)")
|
|
91
|
+
effective_page_size = page_size
|
|
92
|
+
if max_items is not None and (page_size is None or page_size > max_items):
|
|
93
|
+
effective_page_size = max_items
|
|
94
|
+
session = AsanaSession.from_env(paginate=fetch_all, page_size=effective_page_size)
|
|
84
95
|
api = AttachmentsApi(session.client)
|
|
85
96
|
opts: dict[str, Any] = {}
|
|
86
|
-
if limit is not None:
|
|
87
|
-
opts["limit"] = limit
|
|
88
97
|
if offset is not None:
|
|
89
98
|
opts["offset"] = offset
|
|
90
99
|
if opt_fields is not None:
|
|
91
100
|
opts["opt_fields"] = opt_fields
|
|
101
|
+
if max_items is not None:
|
|
102
|
+
return session.fetch_capped(api.get_attachments_for_object, parent, opts=opts, max_items=max_items)
|
|
92
103
|
return api.get_attachments_for_object(parent, opts)
|
|
@@ -6,11 +6,12 @@ from typing import Any
|
|
|
6
6
|
import click
|
|
7
7
|
from asana import AuditLogAPIApi
|
|
8
8
|
|
|
9
|
+
from asana_api_cli.click_ext import GroupWithGlobalOptions
|
|
9
10
|
from asana_api_cli.formatter import formatted
|
|
10
11
|
from asana_api_cli.session import AsanaSession, resolve_body, resolve_workspace
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.group("audit-log-api")
|
|
14
|
+
@click.group("audit-log-api", cls=GroupWithGlobalOptions)
|
|
14
15
|
def audit_log_api_group() -> None:
|
|
15
16
|
"""AuditLogAPI commands."""
|
|
16
17
|
|
|
@@ -21,16 +22,26 @@ def audit_log_api_group() -> None:
|
|
|
21
22
|
@click.option("--actor-type", default=None, help="Filter to events with an actor of this type. This only needs to be included if querying for actor types without an ID. If `actor_gid` is included, this should be excluded.")
|
|
22
23
|
@click.option("--end-at", default=None, help="Filter to events created before this time (exclusive).")
|
|
23
24
|
@click.option("--event-type", default=None, help="Filter to events of this type. Refer to the [supported audit log events](/docs/audit-log-events#supported-audit-log-events) for a full list of values.")
|
|
24
|
-
@click.option("--limit", type=int, default=None, help="Results per page. The number of objects to return per page. The value must be between 1 and 100.")
|
|
25
25
|
@click.option("--offset", default=None, help="Offset token. An offset to the next page returned by the API. A pagination request will return an offset token, which can be used as an input parameter to the next request. If an offset is not pass...")
|
|
26
26
|
@click.option("--resource-gid", default=None, help="Filter to events with this resource ID.")
|
|
27
27
|
@click.option("--start-at", default=None, help="Filter to events created after this time (inclusive).")
|
|
28
|
-
@click.option("--
|
|
28
|
+
@click.option("--all-items", "all_items", is_flag=True, default=False, help="Fetch all items (no cap)")
|
|
29
|
+
@click.option("--paginate", "paginate", is_flag=True, default=False, help="(Deprecated) Alias for --all-items")
|
|
30
|
+
@click.option("--page-size", "page_size", type=int, default=None, help="Items per page (Asana API requires 1-100, default 100)")
|
|
31
|
+
@click.option("--max-items", "max_items", type=int, default=None, help="Stop after fetching this many items in total")
|
|
29
32
|
@formatted
|
|
30
|
-
def get_audit_log_events(workspace: str | None, actor_gid: str | None, actor_type: str | None, end_at: str | None, event_type: str | None,
|
|
33
|
+
def get_audit_log_events(workspace: str | None, actor_gid: str | None, actor_type: str | None, end_at: str | None, event_type: str | None, offset: str | None, resource_gid: str | None, start_at: str | None, all_items: bool, paginate: bool, page_size: int | None, max_items: int | None) -> Any:
|
|
31
34
|
"""Get audit log events"""
|
|
32
35
|
resolved_workspace = resolve_workspace(workspace, required=True)
|
|
33
|
-
|
|
36
|
+
if paginate:
|
|
37
|
+
click.echo("Warning: --paginate is deprecated; use --all-items instead.", err=True)
|
|
38
|
+
fetch_all = all_items or paginate
|
|
39
|
+
if fetch_all and max_items is not None:
|
|
40
|
+
raise click.UsageError("--max-items cannot be combined with --all-items (or its deprecated alias --paginate)")
|
|
41
|
+
effective_page_size = page_size
|
|
42
|
+
if max_items is not None and (page_size is None or page_size > max_items):
|
|
43
|
+
effective_page_size = max_items
|
|
44
|
+
session = AsanaSession.from_env(paginate=fetch_all, page_size=effective_page_size)
|
|
34
45
|
api = AuditLogAPIApi(session.client)
|
|
35
46
|
opts: dict[str, Any] = {}
|
|
36
47
|
if actor_gid is not None:
|
|
@@ -41,12 +52,12 @@ def get_audit_log_events(workspace: str | None, actor_gid: str | None, actor_typ
|
|
|
41
52
|
opts["end_at"] = end_at
|
|
42
53
|
if event_type is not None:
|
|
43
54
|
opts["event_type"] = event_type
|
|
44
|
-
if limit is not None:
|
|
45
|
-
opts["limit"] = limit
|
|
46
55
|
if offset is not None:
|
|
47
56
|
opts["offset"] = offset
|
|
48
57
|
if resource_gid is not None:
|
|
49
58
|
opts["resource_gid"] = resource_gid
|
|
50
59
|
if start_at is not None:
|
|
51
60
|
opts["start_at"] = start_at
|
|
61
|
+
if max_items is not None:
|
|
62
|
+
return session.fetch_capped(api.get_audit_log_events, resolved_workspace, opts=opts, max_items=max_items)
|
|
52
63
|
return api.get_audit_log_events(resolved_workspace, opts)
|
|
@@ -6,11 +6,12 @@ from typing import Any
|
|
|
6
6
|
import click
|
|
7
7
|
from asana import BatchAPIApi
|
|
8
8
|
|
|
9
|
+
from asana_api_cli.click_ext import GroupWithGlobalOptions
|
|
9
10
|
from asana_api_cli.formatter import formatted
|
|
10
11
|
from asana_api_cli.session import AsanaSession, resolve_body, resolve_workspace
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.group("batch-api")
|
|
14
|
+
@click.group("batch-api", cls=GroupWithGlobalOptions)
|
|
14
15
|
def batch_api_group() -> None:
|
|
15
16
|
"""BatchAPI commands."""
|
|
16
17
|
|
|
@@ -6,11 +6,12 @@ from typing import Any
|
|
|
6
6
|
import click
|
|
7
7
|
from asana import BudgetsApi
|
|
8
8
|
|
|
9
|
+
from asana_api_cli.click_ext import GroupWithGlobalOptions
|
|
9
10
|
from asana_api_cli.formatter import formatted
|
|
10
11
|
from asana_api_cli.session import AsanaSession, resolve_body, resolve_workspace
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.group("budgets")
|
|
14
|
+
@click.group("budgets", cls=GroupWithGlobalOptions)
|
|
14
15
|
def budgets_group() -> None:
|
|
15
16
|
"""Budgets commands."""
|
|
16
17
|
|