atlassian-cli 0.12.0__tar.gz → 0.13.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.
- {atlassian_cli-0.12.0/src/atlassian_cli.egg-info → atlassian_cli-0.13.0}/PKG-INFO +21 -1
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/README.md +20 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/pyproject.toml +2 -1
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/config.py +24 -0
- atlassian_cli-0.13.0/src/atlassian_cli/org.py +117 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0/src/atlassian_cli.egg-info}/PKG-INFO +21 -1
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/SOURCES.txt +2 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/entry_points.txt +1 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_config.py +43 -1
- atlassian_cli-0.13.0/tests/test_org.py +95 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/LICENSE +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/setup.cfg +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/__init__.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/adf.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/confluence.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/hints.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/http.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/jira.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/jira_assets.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/jira_issues.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/output.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/update_check.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/dependency_links.txt +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/requires.txt +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/top_level.txt +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_adf.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_confluence.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_http.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_jira_assets.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_jira_issues.py +0 -0
- {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlassian-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents
|
|
5
5
|
Author-email: Alex Fishlock <alex.fishlock@catapult.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,6 +85,12 @@ Get your API token at https://id.atlassian.com/manage-profile/security/api-token
|
|
|
85
85
|
|
|
86
86
|
> Legacy `CONFLUENCE_URL` / `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` are also supported.
|
|
87
87
|
|
|
88
|
+
Organisation admin commands (`org`) use a separate org API key, read from the same config file or env:
|
|
89
|
+
```
|
|
90
|
+
ATLASSIAN_ORG_API_KEY=ATCTT... # admin.atlassian.com > org > Settings > API keys (personal ATATT tokens get 401)
|
|
91
|
+
ATLASSIAN_ORG_ID=your-org-id # or pass --org-id
|
|
92
|
+
```
|
|
93
|
+
|
|
88
94
|
## Confluence CLI
|
|
89
95
|
|
|
90
96
|
Manages Confluence pages as local JSON files in ADF (Atlassian Document Format). No markdown — ADF preserves every macro, panel, and table perfectly.
|
|
@@ -261,6 +267,18 @@ jira assets delete 123
|
|
|
261
267
|
jira assets type-create 1 "Network Device" --description "Switches and routers"
|
|
262
268
|
```
|
|
263
269
|
|
|
270
|
+
## Organisation admin CLI
|
|
271
|
+
|
|
272
|
+
Read-only access to the org admin API: audit log, managed users, audit action types.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
org events --from <epoch ms> --to <epoch ms> [--org-id ID] [--dump FILE] # org audit log events
|
|
276
|
+
org users [--org-id ID] [--dump FILE] # managed accounts with last_active
|
|
277
|
+
org event-actions [--org-id ID] [--dump FILE] # every audit event action type
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`--dump` writes `{"total": N, "<key>": [...]}`. Audit retention is about 180 days.
|
|
281
|
+
|
|
264
282
|
## `--json` flag
|
|
265
283
|
|
|
266
284
|
Both CLIs accept a global `--json` flag that switches all output to machine-readable JSON. Perfect for piping into `jq` or parsing from code.
|
|
@@ -298,12 +316,14 @@ src/atlassian_cli/
|
|
|
298
316
|
jira.py Jira CLI entry point (subparsers)
|
|
299
317
|
jira_issues.py Jira issue commands (v3 API)
|
|
300
318
|
jira_assets.py Jira Assets commands (Assets v1 API)
|
|
319
|
+
org.py Org admin commands (admin v1 API)
|
|
301
320
|
```
|
|
302
321
|
|
|
303
322
|
**APIs used:**
|
|
304
323
|
- Confluence Cloud REST API v2 (`/wiki/api/v2/`)
|
|
305
324
|
- Jira Cloud REST API v3 (`/rest/api/3/`)
|
|
306
325
|
- Jira Assets REST API v1 (`api.atlassian.com/jsm/assets/workspace/{id}/v1`)
|
|
326
|
+
- Organisation admin API v1 (`api.atlassian.com/admin/v1/orgs/{orgId}`)
|
|
307
327
|
|
|
308
328
|
## Development
|
|
309
329
|
|
|
@@ -54,6 +54,12 @@ Get your API token at https://id.atlassian.com/manage-profile/security/api-token
|
|
|
54
54
|
|
|
55
55
|
> Legacy `CONFLUENCE_URL` / `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` are also supported.
|
|
56
56
|
|
|
57
|
+
Organisation admin commands (`org`) use a separate org API key, read from the same config file or env:
|
|
58
|
+
```
|
|
59
|
+
ATLASSIAN_ORG_API_KEY=ATCTT... # admin.atlassian.com > org > Settings > API keys (personal ATATT tokens get 401)
|
|
60
|
+
ATLASSIAN_ORG_ID=your-org-id # or pass --org-id
|
|
61
|
+
```
|
|
62
|
+
|
|
57
63
|
## Confluence CLI
|
|
58
64
|
|
|
59
65
|
Manages Confluence pages as local JSON files in ADF (Atlassian Document Format). No markdown — ADF preserves every macro, panel, and table perfectly.
|
|
@@ -230,6 +236,18 @@ jira assets delete 123
|
|
|
230
236
|
jira assets type-create 1 "Network Device" --description "Switches and routers"
|
|
231
237
|
```
|
|
232
238
|
|
|
239
|
+
## Organisation admin CLI
|
|
240
|
+
|
|
241
|
+
Read-only access to the org admin API: audit log, managed users, audit action types.
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
org events --from <epoch ms> --to <epoch ms> [--org-id ID] [--dump FILE] # org audit log events
|
|
245
|
+
org users [--org-id ID] [--dump FILE] # managed accounts with last_active
|
|
246
|
+
org event-actions [--org-id ID] [--dump FILE] # every audit event action type
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`--dump` writes `{"total": N, "<key>": [...]}`. Audit retention is about 180 days.
|
|
250
|
+
|
|
233
251
|
## `--json` flag
|
|
234
252
|
|
|
235
253
|
Both CLIs accept a global `--json` flag that switches all output to machine-readable JSON. Perfect for piping into `jq` or parsing from code.
|
|
@@ -267,12 +285,14 @@ src/atlassian_cli/
|
|
|
267
285
|
jira.py Jira CLI entry point (subparsers)
|
|
268
286
|
jira_issues.py Jira issue commands (v3 API)
|
|
269
287
|
jira_assets.py Jira Assets commands (Assets v1 API)
|
|
288
|
+
org.py Org admin commands (admin v1 API)
|
|
270
289
|
```
|
|
271
290
|
|
|
272
291
|
**APIs used:**
|
|
273
292
|
- Confluence Cloud REST API v2 (`/wiki/api/v2/`)
|
|
274
293
|
- Jira Cloud REST API v3 (`/rest/api/3/`)
|
|
275
294
|
- Jira Assets REST API v1 (`api.atlassian.com/jsm/assets/workspace/{id}/v1`)
|
|
295
|
+
- Organisation admin API v1 (`api.atlassian.com/admin/v1/orgs/{orgId}`)
|
|
276
296
|
|
|
277
297
|
## Development
|
|
278
298
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "atlassian-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.0"
|
|
8
8
|
description = "Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -45,6 +45,7 @@ Issues = "https://github.com/catapultcx/atlassian-cli/issues"
|
|
|
45
45
|
[project.scripts]
|
|
46
46
|
confluence = "atlassian_cli.confluence:main"
|
|
47
47
|
jira = "atlassian_cli.jira:main"
|
|
48
|
+
org = "atlassian_cli.org:main"
|
|
48
49
|
|
|
49
50
|
[tool.setuptools.packages.find]
|
|
50
51
|
where = ["src"]
|
|
@@ -77,3 +77,27 @@ def setup():
|
|
|
77
77
|
url, email, token = get_config()
|
|
78
78
|
session = get_session(email, token)
|
|
79
79
|
return session, url
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
ORG_API = 'https://api.atlassian.com/admin/v1/orgs'
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_org_config(org_id=None):
|
|
86
|
+
env = load_env()
|
|
87
|
+
api_key = env.get('ATLASSIAN_ORG_API_KEY', os.environ.get('ATLASSIAN_ORG_API_KEY'))
|
|
88
|
+
org_id = org_id or env.get('ATLASSIAN_ORG_ID', os.environ.get('ATLASSIAN_ORG_ID'))
|
|
89
|
+
if not api_key:
|
|
90
|
+
sys.stderr.write('ERR Missing ATLASSIAN_ORG_API_KEY (organisation admin API key)\n'
|
|
91
|
+
'Set it in .env or as an environment variable.\n')
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
if not org_id:
|
|
94
|
+
sys.stderr.write('ERR Missing ATLASSIAN_ORG_ID: set it in .env or pass --org-id.\n')
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
return api_key.strip(), org_id.strip()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def org_setup(org_id=None):
|
|
100
|
+
api_key, org_id = get_org_config(org_id)
|
|
101
|
+
session = requests.Session()
|
|
102
|
+
session.headers.update({'Authorization': f'Bearer {api_key}', 'Accept': 'application/json'})
|
|
103
|
+
return session, f'{ORG_API}/{org_id}'
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Atlassian organisation admin CLI: audit log events, managed users, event action types.
|
|
3
|
+
|
|
4
|
+
Uses the organisation admin API (api.atlassian.com/admin/v1/orgs/{orgId}) with an org API key,
|
|
5
|
+
which is separate from the site credentials the confluence and jira commands use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from atlassian_cli.config import org_setup
|
|
13
|
+
from atlassian_cli.http import APIError, _retry
|
|
14
|
+
from atlassian_cli.output import emit, emit_error, is_json_mode, set_json_mode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get(session, url, params=None):
|
|
18
|
+
response = _retry(session.get, url, params=params)
|
|
19
|
+
if response.ok:
|
|
20
|
+
return response.json()
|
|
21
|
+
raise APIError(response.status_code, response.text)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fetch_events(session, base, start_ms, end_ms):
|
|
25
|
+
# events-stream drops out of the window if a cursor is sent without from/to.
|
|
26
|
+
window = {'from': start_ms, 'to': end_ms}
|
|
27
|
+
params = dict(window)
|
|
28
|
+
events = []
|
|
29
|
+
while True:
|
|
30
|
+
data = _get(session, f'{base}/events-stream', params)
|
|
31
|
+
batch = data.get('data') or []
|
|
32
|
+
events.extend(batch)
|
|
33
|
+
cursor = (data.get('meta') or {}).get('next')
|
|
34
|
+
if not batch or not cursor:
|
|
35
|
+
return events
|
|
36
|
+
params = dict(window, cursor=cursor)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fetch_list(session, url):
|
|
40
|
+
items = []
|
|
41
|
+
while url:
|
|
42
|
+
data = _get(session, url)
|
|
43
|
+
items.extend(data.get('data') or [])
|
|
44
|
+
url = (data.get('links') or {}).get('next')
|
|
45
|
+
return items
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _finish(args, key, items, line):
|
|
49
|
+
if args.dump:
|
|
50
|
+
with open(args.dump, 'w') as fh:
|
|
51
|
+
json.dump({'total': len(items), key: items}, fh, indent=2)
|
|
52
|
+
elif not is_json_mode():
|
|
53
|
+
for item in items:
|
|
54
|
+
print(line(item))
|
|
55
|
+
emit('DONE', f'{len(items)} {key}', data={'total': len(items)})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_events(args):
|
|
59
|
+
session, base = org_setup(args.org_id)
|
|
60
|
+
events = fetch_events(session, base, args.start_ms, args.end_ms)
|
|
61
|
+
_finish(args, 'events', events, lambda e: ' '.join([
|
|
62
|
+
e['attributes'].get('time', ''), e['attributes'].get('action', ''),
|
|
63
|
+
(e['attributes'].get('actor') or {}).get('name', '')]))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_users(args):
|
|
67
|
+
session, base = org_setup(args.org_id)
|
|
68
|
+
users = fetch_list(session, f'{base}/users')
|
|
69
|
+
_finish(args, 'users', users,
|
|
70
|
+
lambda u: f"{u.get('account_id', '')} {u.get('email', '')} {u.get('last_active', '')}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_event_actions(args):
|
|
74
|
+
session, base = org_setup(args.org_id)
|
|
75
|
+
actions = fetch_list(session, f'{base}/event-actions')
|
|
76
|
+
_finish(args, 'actions', actions, lambda a: a.get('id', ''))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
from atlassian_cli.update_check import check_for_update
|
|
81
|
+
check_for_update()
|
|
82
|
+
|
|
83
|
+
if '--json' in sys.argv:
|
|
84
|
+
sys.argv.remove('--json')
|
|
85
|
+
set_json_mode(True)
|
|
86
|
+
|
|
87
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
88
|
+
common.add_argument('--org-id', help='Organisation id (default: ATLASSIAN_ORG_ID from config)')
|
|
89
|
+
common.add_argument('--dump', metavar='FILE', help='Save the full data to a JSON file')
|
|
90
|
+
|
|
91
|
+
parser = argparse.ArgumentParser(prog='org',
|
|
92
|
+
description='Atlassian organisation admin: audit events, users, event actions')
|
|
93
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
94
|
+
|
|
95
|
+
p = sub.add_parser('events', parents=[common], help='Audit log events in a time window')
|
|
96
|
+
p.add_argument('--from', dest='start_ms', type=int, required=True, help='Window start, epoch milliseconds')
|
|
97
|
+
p.add_argument('--to', dest='end_ms', type=int, required=True, help='Window end, epoch milliseconds')
|
|
98
|
+
p.set_defaults(func=cmd_events)
|
|
99
|
+
|
|
100
|
+
p = sub.add_parser('users', parents=[common], help='Managed accounts with last-active dates')
|
|
101
|
+
p.set_defaults(func=cmd_users)
|
|
102
|
+
|
|
103
|
+
p = sub.add_parser('event-actions', parents=[common], help='Every audit event action type')
|
|
104
|
+
p.set_defaults(func=cmd_event_actions)
|
|
105
|
+
|
|
106
|
+
args = parser.parse_args()
|
|
107
|
+
try:
|
|
108
|
+
args.func(args)
|
|
109
|
+
except APIError as e:
|
|
110
|
+
emit_error(str(e))
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
except KeyboardInterrupt:
|
|
113
|
+
sys.exit(130)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == '__main__':
|
|
117
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlassian-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents
|
|
5
5
|
Author-email: Alex Fishlock <alex.fishlock@catapult.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,6 +85,12 @@ Get your API token at https://id.atlassian.com/manage-profile/security/api-token
|
|
|
85
85
|
|
|
86
86
|
> Legacy `CONFLUENCE_URL` / `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` are also supported.
|
|
87
87
|
|
|
88
|
+
Organisation admin commands (`org`) use a separate org API key, read from the same config file or env:
|
|
89
|
+
```
|
|
90
|
+
ATLASSIAN_ORG_API_KEY=ATCTT... # admin.atlassian.com > org > Settings > API keys (personal ATATT tokens get 401)
|
|
91
|
+
ATLASSIAN_ORG_ID=your-org-id # or pass --org-id
|
|
92
|
+
```
|
|
93
|
+
|
|
88
94
|
## Confluence CLI
|
|
89
95
|
|
|
90
96
|
Manages Confluence pages as local JSON files in ADF (Atlassian Document Format). No markdown — ADF preserves every macro, panel, and table perfectly.
|
|
@@ -261,6 +267,18 @@ jira assets delete 123
|
|
|
261
267
|
jira assets type-create 1 "Network Device" --description "Switches and routers"
|
|
262
268
|
```
|
|
263
269
|
|
|
270
|
+
## Organisation admin CLI
|
|
271
|
+
|
|
272
|
+
Read-only access to the org admin API: audit log, managed users, audit action types.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
org events --from <epoch ms> --to <epoch ms> [--org-id ID] [--dump FILE] # org audit log events
|
|
276
|
+
org users [--org-id ID] [--dump FILE] # managed accounts with last_active
|
|
277
|
+
org event-actions [--org-id ID] [--dump FILE] # every audit event action type
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`--dump` writes `{"total": N, "<key>": [...]}`. Audit retention is about 180 days.
|
|
281
|
+
|
|
264
282
|
## `--json` flag
|
|
265
283
|
|
|
266
284
|
Both CLIs accept a global `--json` flag that switches all output to machine-readable JSON. Perfect for piping into `jq` or parsing from code.
|
|
@@ -298,12 +316,14 @@ src/atlassian_cli/
|
|
|
298
316
|
jira.py Jira CLI entry point (subparsers)
|
|
299
317
|
jira_issues.py Jira issue commands (v3 API)
|
|
300
318
|
jira_assets.py Jira Assets commands (Assets v1 API)
|
|
319
|
+
org.py Org admin commands (admin v1 API)
|
|
301
320
|
```
|
|
302
321
|
|
|
303
322
|
**APIs used:**
|
|
304
323
|
- Confluence Cloud REST API v2 (`/wiki/api/v2/`)
|
|
305
324
|
- Jira Cloud REST API v3 (`/rest/api/3/`)
|
|
306
325
|
- Jira Assets REST API v1 (`api.atlassian.com/jsm/assets/workspace/{id}/v1`)
|
|
326
|
+
- Organisation admin API v1 (`api.atlassian.com/admin/v1/orgs/{orgId}`)
|
|
307
327
|
|
|
308
328
|
## Development
|
|
309
329
|
|
|
@@ -10,6 +10,7 @@ src/atlassian_cli/http.py
|
|
|
10
10
|
src/atlassian_cli/jira.py
|
|
11
11
|
src/atlassian_cli/jira_assets.py
|
|
12
12
|
src/atlassian_cli/jira_issues.py
|
|
13
|
+
src/atlassian_cli/org.py
|
|
13
14
|
src/atlassian_cli/output.py
|
|
14
15
|
src/atlassian_cli/update_check.py
|
|
15
16
|
src/atlassian_cli.egg-info/PKG-INFO
|
|
@@ -24,4 +25,5 @@ tests/test_confluence.py
|
|
|
24
25
|
tests/test_http.py
|
|
25
26
|
tests/test_jira_assets.py
|
|
26
27
|
tests/test_jira_issues.py
|
|
28
|
+
tests/test_org.py
|
|
27
29
|
tests/test_output.py
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
from atlassian_cli.config import get_config, get_session, load_env, setup
|
|
5
|
+
from atlassian_cli.config import get_config, get_org_config, get_session, load_env, org_setup, setup
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TestLoadEnv:
|
|
@@ -130,3 +130,45 @@ class TestSetup:
|
|
|
130
130
|
session, base = setup()
|
|
131
131
|
assert base == "https://test.atlassian.net"
|
|
132
132
|
assert session.auth is not None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestGetOrgConfig:
|
|
136
|
+
@pytest.fixture(autouse=True)
|
|
137
|
+
def _isolate(self, monkeypatch, tmp_path):
|
|
138
|
+
monkeypatch.chdir(tmp_path)
|
|
139
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
140
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
141
|
+
monkeypatch.delenv("ATLASSIAN_CLI_CONFIG", raising=False)
|
|
142
|
+
monkeypatch.delenv("ATLASSIAN_ORG_API_KEY", raising=False)
|
|
143
|
+
monkeypatch.delenv("ATLASSIAN_ORG_ID", raising=False)
|
|
144
|
+
|
|
145
|
+
def test_reads_key_and_org_id_from_config(self, tmp_path):
|
|
146
|
+
(tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\nATLASSIAN_ORG_ID=org-1\n")
|
|
147
|
+
assert get_org_config() == ("ATCTT-k", "org-1")
|
|
148
|
+
|
|
149
|
+
def test_explicit_org_id_wins(self, tmp_path):
|
|
150
|
+
(tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\nATLASSIAN_ORG_ID=org-1\n")
|
|
151
|
+
assert get_org_config("org-2") == ("ATCTT-k", "org-2")
|
|
152
|
+
|
|
153
|
+
def test_env_var_fallback(self, monkeypatch):
|
|
154
|
+
monkeypatch.setenv("ATLASSIAN_ORG_API_KEY", "ATCTT-e")
|
|
155
|
+
monkeypatch.setenv("ATLASSIAN_ORG_ID", "org-e")
|
|
156
|
+
assert get_org_config() == ("ATCTT-e", "org-e")
|
|
157
|
+
|
|
158
|
+
def test_missing_key_exits_naming_it(self, capsys):
|
|
159
|
+
with pytest.raises(SystemExit):
|
|
160
|
+
get_org_config("org-1")
|
|
161
|
+
assert "ATLASSIAN_ORG_API_KEY" in capsys.readouterr().err
|
|
162
|
+
|
|
163
|
+
def test_missing_org_id_exits_naming_it(self, tmp_path, capsys):
|
|
164
|
+
(tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\n")
|
|
165
|
+
with pytest.raises(SystemExit):
|
|
166
|
+
get_org_config()
|
|
167
|
+
assert "ATLASSIAN_ORG_ID" in capsys.readouterr().err
|
|
168
|
+
|
|
169
|
+
def test_org_setup_uses_bearer_auth(self, tmp_path):
|
|
170
|
+
(tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\n")
|
|
171
|
+
session, base = org_setup("org-1")
|
|
172
|
+
assert session.headers["Authorization"] == "Bearer ATCTT-k"
|
|
173
|
+
assert session.auth is None
|
|
174
|
+
assert base == "https://api.atlassian.com/admin/v1/orgs/org-1"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Tests for atlassian_cli.org."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from urllib.parse import parse_qsl, urlparse
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from atlassian_cli import org
|
|
10
|
+
from atlassian_cli.http import APIError
|
|
11
|
+
from atlassian_cli.output import set_json_mode
|
|
12
|
+
|
|
13
|
+
ORG_BASE = "https://api.atlassian.com/admin/v1/orgs/org-1"
|
|
14
|
+
EVENTS = f"{ORG_BASE}/events-stream"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _patch_setup(monkeypatch, mock_session):
|
|
19
|
+
monkeypatch.setattr("atlassian_cli.org.org_setup", lambda org_id=None: (mock_session, ORG_BASE))
|
|
20
|
+
set_json_mode(False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def slept(monkeypatch):
|
|
25
|
+
calls = []
|
|
26
|
+
monkeypatch.setattr("atlassian_cli.http.time.sleep", calls.append)
|
|
27
|
+
return calls
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _params(call):
|
|
31
|
+
return dict(parse_qsl(urlparse(call.request.url).query))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestFetchEvents:
|
|
35
|
+
def test_pages_by_cursor_keeping_the_window(self, mock_session, mocked_responses):
|
|
36
|
+
mocked_responses.get(EVENTS, json={"data": [{"id": "1"}, {"id": "2"}], "meta": {"next": "c1"}})
|
|
37
|
+
mocked_responses.get(EVENTS, json={"data": [{"id": "3"}], "meta": {"next": "c2"}})
|
|
38
|
+
mocked_responses.get(EVENTS, json={"data": [], "meta": {}})
|
|
39
|
+
events = org.fetch_events(mock_session, ORG_BASE, 1000, 2000)
|
|
40
|
+
assert [e["id"] for e in events] == ["1", "2", "3"]
|
|
41
|
+
params = [_params(c) for c in mocked_responses.calls]
|
|
42
|
+
assert all(p["from"] == "1000" and p["to"] == "2000" for p in params)
|
|
43
|
+
assert [p.get("cursor") for p in params] == [None, "c1", "c2"]
|
|
44
|
+
|
|
45
|
+
def test_retries_429_with_the_same_request(self, mock_session, mocked_responses, slept):
|
|
46
|
+
mocked_responses.get(EVENTS, json={"data": [{"id": "1"}], "meta": {"next": "c1"}})
|
|
47
|
+
mocked_responses.get(EVENTS, status=429, headers={"Retry-After": "3"})
|
|
48
|
+
mocked_responses.get(EVENTS, json={"data": [{"id": "2"}], "meta": {}})
|
|
49
|
+
events = org.fetch_events(mock_session, ORG_BASE, 1000, 2000)
|
|
50
|
+
assert [e["id"] for e in events] == ["1", "2"]
|
|
51
|
+
params = [_params(c) for c in mocked_responses.calls]
|
|
52
|
+
assert [p.get("cursor") for p in params] == [None, "c1", "c1"]
|
|
53
|
+
assert all(p["from"] == "1000" and p["to"] == "2000" for p in params)
|
|
54
|
+
assert len(slept) == 1 and 2.1 <= slept[0] <= 3.9
|
|
55
|
+
|
|
56
|
+
def test_http_error_raises(self, mock_session, mocked_responses):
|
|
57
|
+
mocked_responses.get(EVENTS, status=401, body="unauthorised")
|
|
58
|
+
with pytest.raises(APIError):
|
|
59
|
+
org.fetch_events(mock_session, ORG_BASE, 1000, 2000)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestFetchList:
|
|
63
|
+
def test_follows_absolute_next_links(self, mock_session, mocked_responses):
|
|
64
|
+
mocked_responses.get(f"{ORG_BASE}/users", json={"data": [{"account_id": "a"}], "links": {"next": "https://n.example/p2"}})
|
|
65
|
+
mocked_responses.get("https://n.example/p2", json={"data": [{"account_id": "b"}], "links": {}})
|
|
66
|
+
assert [u["account_id"] for u in org.fetch_list(mock_session, f"{ORG_BASE}/users")] == ["a", "b"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestCommands:
|
|
70
|
+
def test_events_writes_the_dump(self, mocked_responses, tmp_path):
|
|
71
|
+
mocked_responses.get(EVENTS, json={"data": [{"id": "1"}], "meta": {}})
|
|
72
|
+
dump = tmp_path / "events.json"
|
|
73
|
+
org.cmd_events(Namespace(org_id="org-1", start_ms=1000, end_ms=2000, dump=str(dump)))
|
|
74
|
+
assert json.loads(dump.read_text()) == {"total": 1, "events": [{"id": "1"}]}
|
|
75
|
+
|
|
76
|
+
def test_users_writes_the_dump(self, mocked_responses, tmp_path):
|
|
77
|
+
mocked_responses.get(f"{ORG_BASE}/users", json={"data": [{"account_id": "a", "last_active": "x"}], "links": {}})
|
|
78
|
+
dump = tmp_path / "users.json"
|
|
79
|
+
org.cmd_users(Namespace(org_id=None, dump=str(dump)))
|
|
80
|
+
assert json.loads(dump.read_text())["users"] == [{"account_id": "a", "last_active": "x"}]
|
|
81
|
+
|
|
82
|
+
def test_event_actions_writes_the_dump(self, mocked_responses, tmp_path):
|
|
83
|
+
mocked_responses.get(f"{ORG_BASE}/event-actions", json={"data": [{"id": "x"}], "links": {}})
|
|
84
|
+
dump = tmp_path / "actions.json"
|
|
85
|
+
org.cmd_event_actions(Namespace(org_id=None, dump=str(dump)))
|
|
86
|
+
assert json.loads(dump.read_text())["actions"] == [{"id": "x"}]
|
|
87
|
+
|
|
88
|
+
def test_main_parses_the_window_and_org_id(self, monkeypatch):
|
|
89
|
+
seen = []
|
|
90
|
+
monkeypatch.setattr("atlassian_cli.update_check.check_for_update", lambda: None)
|
|
91
|
+
monkeypatch.setattr("atlassian_cli.org.cmd_events", seen.append)
|
|
92
|
+
monkeypatch.setattr("sys.argv", ["org", "events", "--from", "1", "--to", "2",
|
|
93
|
+
"--org-id", "org-9", "--dump", "f.json", "--json"])
|
|
94
|
+
org.main()
|
|
95
|
+
assert (seen[0].start_ms, seen[0].end_ms, seen[0].org_id, seen[0].dump) == (1, 2, "org-9", "f.json")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|