current-cli 0.1.58__tar.gz → 0.1.60__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.
- {current_cli-0.1.58 → current_cli-0.1.60}/PKG-INFO +8 -1
- {current_cli-0.1.58 → current_cli-0.1.60}/README.md +7 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/docs.py +5 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/main.py +83 -2
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/schema.yml +358 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/llms.txt +27 -1
- {current_cli-0.1.58 → current_cli-0.1.60}/pyproject.toml +1 -1
- current_cli-0.1.60/tests/test_auth.py +213 -0
- current_cli-0.1.58/tests/test_auth.py +0 -92
- {current_cli-0.1.58 → current_cli-0.1.60}/.gitignore +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/__init__.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/build.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/config.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/http.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/current_cli/schema.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/scripts/bump_version.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/tests/__init__.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/tests/conftest.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/tests/test_bump_version.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/tests/test_command_tree.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/tests/test_docs.py +0 -0
- {current_cli-0.1.58 → current_cli-0.1.60}/tests/test_requests.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: current-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.60
|
|
4
4
|
Summary: Command line interface for the Current API, generated from its OpenAPI schema at runtime
|
|
5
5
|
Author-email: Orin <your-email@example.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,6 +41,13 @@ current tasks create --data '{"name": "Order beams", "workspace": "..."}'
|
|
|
41
41
|
current workspaces members list <workspace_id>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
`current login` emails a magic link: paste the whole link (or just its token)
|
|
45
|
+
when asked. Accounts that require two-factor authentication — Orin Labs
|
|
46
|
+
superadmins, workspace admins, and members of workspaces that require it —
|
|
47
|
+
are then asked for an authenticator code. An account with no authenticator
|
|
48
|
+
enrolled yet is shown a setup link and secret key to add to an authenticator
|
|
49
|
+
app, and confirms the first code to finish signing in.
|
|
50
|
+
|
|
44
51
|
The CLI points at `https://api.current.orinlabs.ai` by default. For local
|
|
45
52
|
development, run `current config set base_url http://localhost:8000`.
|
|
46
53
|
|
|
@@ -28,6 +28,13 @@ current tasks create --data '{"name": "Order beams", "workspace": "..."}'
|
|
|
28
28
|
current workspaces members list <workspace_id>
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`current login` emails a magic link: paste the whole link (or just its token)
|
|
32
|
+
when asked. Accounts that require two-factor authentication — Orin Labs
|
|
33
|
+
superadmins, workspace admins, and members of workspaces that require it —
|
|
34
|
+
are then asked for an authenticator code. An account with no authenticator
|
|
35
|
+
enrolled yet is shown a setup link and secret key to add to an authenticator
|
|
36
|
+
app, and confirms the first code to finish signing in.
|
|
37
|
+
|
|
31
38
|
The CLI points at `https://api.current.orinlabs.ai` by default. For local
|
|
32
39
|
development, run `current config set base_url http://localhost:8000`.
|
|
33
40
|
|
|
@@ -24,6 +24,11 @@ current login you@example.com # magic-link sign in, sto
|
|
|
24
24
|
current config set default_workspace <workspace-uuid> # used when --workspace is omitted
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
`current login` asks for the sign-in link from the email (or its token). An
|
|
28
|
+
account that requires two-factor authentication is also asked for an
|
|
29
|
+
authenticator code; one with no device enrolled yet is shown a setup link
|
|
30
|
+
and secret key to add to an authenticator app first.
|
|
31
|
+
|
|
27
32
|
The CLI speaks to `https://api.current.orinlabs.ai` by default. For another
|
|
28
33
|
server (for example local development), set
|
|
29
34
|
`current config set base_url http://localhost:8000`.
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
from urllib.parse import parse_qs, urlsplit
|
|
7
8
|
|
|
8
9
|
import click
|
|
9
10
|
|
|
@@ -16,12 +17,87 @@ MAGIC_LINK_PATH = "/api/accounts/auth/magic-link/"
|
|
|
16
17
|
VERIFY_PATH = "/api/accounts/auth/verify/"
|
|
17
18
|
LOGOUT_PATH = "/api/accounts/auth/logout/"
|
|
18
19
|
ME_PATH = "/api/accounts/auth/me/"
|
|
20
|
+
MFA_VERIFY_PATH = "/api/accounts/auth/mfa/verify/"
|
|
21
|
+
MFA_SETUP_PATH = "/api/accounts/auth/mfa/setup/"
|
|
22
|
+
MFA_CONFIRM_PATH = "/api/accounts/auth/mfa/confirm/"
|
|
23
|
+
|
|
24
|
+
# Tries at the authenticator code before login gives up. The pending-login
|
|
25
|
+
# ticket outlives these by far (10 minutes), so a typo costs a retry, not
|
|
26
|
+
# another magic link.
|
|
27
|
+
MFA_MAX_ATTEMPTS = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _magic_token(value: str) -> str:
|
|
31
|
+
"""The magic-link token from what the user pasted: the whole sign-in link
|
|
32
|
+
from the email (``.../auth/verify?token=...``) or the bare token."""
|
|
33
|
+
value = value.strip()
|
|
34
|
+
if "token=" in value:
|
|
35
|
+
tokens = parse_qs(urlsplit(value).query or value).get("token")
|
|
36
|
+
if tokens:
|
|
37
|
+
return tokens[0]
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _detail(response) -> str:
|
|
42
|
+
try:
|
|
43
|
+
body = response.json()
|
|
44
|
+
except ValueError:
|
|
45
|
+
return response.text
|
|
46
|
+
if isinstance(body, dict) and isinstance(body.get("detail"), str):
|
|
47
|
+
return body["detail"]
|
|
48
|
+
return response.text
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _fail(response) -> None:
|
|
52
|
+
"""Exit on a failed sign-in step. An expired pending-login ticket gets a
|
|
53
|
+
plain instruction instead of the raw API error."""
|
|
54
|
+
if "ticket" in _detail(response).lower():
|
|
55
|
+
click.echo("The sign-in expired. Run `current login` again.", err=True)
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
http.render_response(response)
|
|
58
|
+
raise SystemExit(1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _second_factor(ticket: str, enrolled: bool) -> dict:
|
|
62
|
+
"""Finish a sign-in that needs two-factor authentication and return the
|
|
63
|
+
login payload. An enrolled account verifies a code; an account without a
|
|
64
|
+
device enrolls one first, and confirming it completes the sign-in."""
|
|
65
|
+
if enrolled:
|
|
66
|
+
click.echo("This account requires two-factor authentication.")
|
|
67
|
+
path = MFA_VERIFY_PATH
|
|
68
|
+
else:
|
|
69
|
+
setup = http.request(
|
|
70
|
+
"post", MFA_SETUP_PATH, json_body={"ticket": ticket}, authenticated=False
|
|
71
|
+
)
|
|
72
|
+
if setup.status_code >= 400:
|
|
73
|
+
_fail(setup)
|
|
74
|
+
enrollment = setup.json()
|
|
75
|
+
click.echo("This account requires two-factor authentication. Add it to an")
|
|
76
|
+
click.echo("authenticator app with the setup link or the secret key:")
|
|
77
|
+
click.echo(f" Setup link: {enrollment['otpauth_url']}")
|
|
78
|
+
click.echo(f" Secret key: {enrollment['secret']}")
|
|
79
|
+
path = MFA_CONFIRM_PATH
|
|
80
|
+
|
|
81
|
+
for attempt in range(1, MFA_MAX_ATTEMPTS + 1):
|
|
82
|
+
code = "".join(click.prompt("Authenticator code").split())
|
|
83
|
+
response = http.request(
|
|
84
|
+
"post", path, json_body={"ticket": ticket, "code": code}, authenticated=False
|
|
85
|
+
)
|
|
86
|
+
if response.status_code < 400:
|
|
87
|
+
return response.json()
|
|
88
|
+
retryable = response.status_code == 400 and "ticket" not in _detail(response).lower()
|
|
89
|
+
if retryable and attempt < MFA_MAX_ATTEMPTS:
|
|
90
|
+
click.echo(f"{_detail(response)} Try again.", err=True)
|
|
91
|
+
continue
|
|
92
|
+
_fail(response)
|
|
93
|
+
raise AssertionError("unreachable")
|
|
19
94
|
|
|
20
95
|
|
|
21
96
|
@click.command()
|
|
22
97
|
@click.argument("email")
|
|
23
98
|
def login(email: str) -> None:
|
|
24
|
-
"""Sign in with a magic link and
|
|
99
|
+
"""Sign in with a magic link (and an authenticator code when the account
|
|
100
|
+
requires two-factor authentication) and store the auth token."""
|
|
25
101
|
response = http.request(
|
|
26
102
|
"post", MAGIC_LINK_PATH, json_body={"email": email}, authenticated=False
|
|
27
103
|
)
|
|
@@ -34,7 +110,7 @@ def login(email: str) -> None:
|
|
|
34
110
|
click.echo("Using debug token from the API (DEBUG mode).")
|
|
35
111
|
else:
|
|
36
112
|
click.echo(f"Magic link sent to {email}.")
|
|
37
|
-
magic_token = click.prompt("Paste the
|
|
113
|
+
magic_token = _magic_token(click.prompt("Paste the sign-in link or its token"))
|
|
38
114
|
|
|
39
115
|
verify = http.request(
|
|
40
116
|
"post", VERIFY_PATH, json_body={"token": magic_token}, authenticated=False
|
|
@@ -44,6 +120,11 @@ def login(email: str) -> None:
|
|
|
44
120
|
return
|
|
45
121
|
|
|
46
122
|
payload = verify.json()
|
|
123
|
+
if payload.get("mfa_required"):
|
|
124
|
+
payload = _second_factor(payload["ticket"], bool(payload.get("mfa_enrolled")))
|
|
125
|
+
|
|
126
|
+
# Saved only once a real token is in hand: a sign-in abandoned part-way
|
|
127
|
+
# leaves the previous config as it was.
|
|
47
128
|
config_store.set_value("token", payload["token"])
|
|
48
129
|
config_store.set_value("email", email)
|
|
49
130
|
click.echo(f"Logged in as {email}.")
|
|
@@ -1973,6 +1973,37 @@ paths:
|
|
|
1973
1973
|
schema:
|
|
1974
1974
|
$ref: '#/components/schemas/OptionUsage'
|
|
1975
1975
|
description: ''
|
|
1976
|
+
/api/projects/field-definitions/query-metadata/:
|
|
1977
|
+
get:
|
|
1978
|
+
operationId: projects_field_definitions_query_metadata_retrieve
|
|
1979
|
+
description: Discover field keys, labels, types, and the workspace clock.
|
|
1980
|
+
parameters:
|
|
1981
|
+
- in: query
|
|
1982
|
+
name: fields
|
|
1983
|
+
schema:
|
|
1984
|
+
type: string
|
|
1985
|
+
description: Optional comma-separated field keys
|
|
1986
|
+
- in: query
|
|
1987
|
+
name: offset
|
|
1988
|
+
schema:
|
|
1989
|
+
type: integer
|
|
1990
|
+
- in: query
|
|
1991
|
+
name: workspace
|
|
1992
|
+
schema:
|
|
1993
|
+
type: string
|
|
1994
|
+
description: Filter by workspace UUID
|
|
1995
|
+
tags:
|
|
1996
|
+
- projects
|
|
1997
|
+
security:
|
|
1998
|
+
- MessagingJobTokenAuth: []
|
|
1999
|
+
- tokenAuth: []
|
|
2000
|
+
responses:
|
|
2001
|
+
'200':
|
|
2002
|
+
content:
|
|
2003
|
+
application/json:
|
|
2004
|
+
schema:
|
|
2005
|
+
$ref: '#/components/schemas/FieldDiscovery'
|
|
2006
|
+
description: ''
|
|
1976
2007
|
/api/projects/projects/:
|
|
1977
2008
|
get:
|
|
1978
2009
|
operationId: projects_projects_list
|
|
@@ -2379,6 +2410,162 @@ paths:
|
|
|
2379
2410
|
schema:
|
|
2380
2411
|
$ref: '#/components/schemas/ProjectInventoryResponse'
|
|
2381
2412
|
description: ''
|
|
2413
|
+
/api/projects/projects/query-fields/:
|
|
2414
|
+
post:
|
|
2415
|
+
operationId: projects_projects_query_fields_create
|
|
2416
|
+
description: Read canonical field snapshots. This POST does not write project
|
|
2417
|
+
state.
|
|
2418
|
+
tags:
|
|
2419
|
+
- projects
|
|
2420
|
+
requestBody:
|
|
2421
|
+
content:
|
|
2422
|
+
application/json:
|
|
2423
|
+
schema:
|
|
2424
|
+
additionalProperties: false
|
|
2425
|
+
properties:
|
|
2426
|
+
fields:
|
|
2427
|
+
items:
|
|
2428
|
+
type: string
|
|
2429
|
+
maxItems: 8
|
|
2430
|
+
minItems: 1
|
|
2431
|
+
title: Fields
|
|
2432
|
+
type: array
|
|
2433
|
+
project_ids:
|
|
2434
|
+
anyOf:
|
|
2435
|
+
- items:
|
|
2436
|
+
type: string
|
|
2437
|
+
maxItems: 200
|
|
2438
|
+
type: array
|
|
2439
|
+
- type: 'null'
|
|
2440
|
+
default: null
|
|
2441
|
+
title: Project Ids
|
|
2442
|
+
as_of:
|
|
2443
|
+
default: latest
|
|
2444
|
+
title: As Of
|
|
2445
|
+
type: string
|
|
2446
|
+
compare_to:
|
|
2447
|
+
anyOf:
|
|
2448
|
+
- type: string
|
|
2449
|
+
- type: 'null'
|
|
2450
|
+
default: null
|
|
2451
|
+
title: Compare To
|
|
2452
|
+
filters:
|
|
2453
|
+
items:
|
|
2454
|
+
additionalProperties: false
|
|
2455
|
+
properties:
|
|
2456
|
+
field:
|
|
2457
|
+
title: Field
|
|
2458
|
+
type: string
|
|
2459
|
+
target:
|
|
2460
|
+
enum:
|
|
2461
|
+
- as_of
|
|
2462
|
+
- compare_to
|
|
2463
|
+
- changed
|
|
2464
|
+
- delta_days
|
|
2465
|
+
title: Target
|
|
2466
|
+
type: string
|
|
2467
|
+
op:
|
|
2468
|
+
enum:
|
|
2469
|
+
- eq
|
|
2470
|
+
- ne
|
|
2471
|
+
- in
|
|
2472
|
+
- not_in
|
|
2473
|
+
- lt
|
|
2474
|
+
- lte
|
|
2475
|
+
- gt
|
|
2476
|
+
- gte
|
|
2477
|
+
- is_empty
|
|
2478
|
+
- is_missing
|
|
2479
|
+
- is_set
|
|
2480
|
+
title: Op
|
|
2481
|
+
type: string
|
|
2482
|
+
value:
|
|
2483
|
+
anyOf:
|
|
2484
|
+
- type: string
|
|
2485
|
+
- type: integer
|
|
2486
|
+
- type: number
|
|
2487
|
+
- type: boolean
|
|
2488
|
+
- items:
|
|
2489
|
+
anyOf:
|
|
2490
|
+
- type: string
|
|
2491
|
+
- type: integer
|
|
2492
|
+
- type: number
|
|
2493
|
+
- type: boolean
|
|
2494
|
+
type: array
|
|
2495
|
+
- type: 'null'
|
|
2496
|
+
title: Value
|
|
2497
|
+
required:
|
|
2498
|
+
- field
|
|
2499
|
+
- target
|
|
2500
|
+
- op
|
|
2501
|
+
- value
|
|
2502
|
+
title: FieldPredicate
|
|
2503
|
+
type: object
|
|
2504
|
+
maxItems: 20
|
|
2505
|
+
title: Filters
|
|
2506
|
+
type: array
|
|
2507
|
+
sort:
|
|
2508
|
+
anyOf:
|
|
2509
|
+
- additionalProperties: false
|
|
2510
|
+
properties:
|
|
2511
|
+
field:
|
|
2512
|
+
title: Field
|
|
2513
|
+
type: string
|
|
2514
|
+
target:
|
|
2515
|
+
enum:
|
|
2516
|
+
- as_of
|
|
2517
|
+
- compare_to
|
|
2518
|
+
- changed
|
|
2519
|
+
- delta_days
|
|
2520
|
+
title: Target
|
|
2521
|
+
type: string
|
|
2522
|
+
direction:
|
|
2523
|
+
enum:
|
|
2524
|
+
- asc
|
|
2525
|
+
- desc
|
|
2526
|
+
title: Direction
|
|
2527
|
+
type: string
|
|
2528
|
+
required:
|
|
2529
|
+
- field
|
|
2530
|
+
- target
|
|
2531
|
+
- direction
|
|
2532
|
+
title: FieldSort
|
|
2533
|
+
type: object
|
|
2534
|
+
- type: 'null'
|
|
2535
|
+
default: null
|
|
2536
|
+
limit:
|
|
2537
|
+
default: 20
|
|
2538
|
+
maximum: 50
|
|
2539
|
+
minimum: 1
|
|
2540
|
+
title: Limit
|
|
2541
|
+
type: integer
|
|
2542
|
+
cursor:
|
|
2543
|
+
anyOf:
|
|
2544
|
+
- maxLength: 12000
|
|
2545
|
+
minLength: 1
|
|
2546
|
+
type: string
|
|
2547
|
+
- type: 'null'
|
|
2548
|
+
default: null
|
|
2549
|
+
title: Cursor
|
|
2550
|
+
workspace:
|
|
2551
|
+
format: uuid
|
|
2552
|
+
title: Workspace
|
|
2553
|
+
type: string
|
|
2554
|
+
required:
|
|
2555
|
+
- fields
|
|
2556
|
+
- workspace
|
|
2557
|
+
title: FieldQueryRequest
|
|
2558
|
+
type: object
|
|
2559
|
+
security:
|
|
2560
|
+
- MessagingJobTokenAuth: []
|
|
2561
|
+
- tokenAuth: []
|
|
2562
|
+
responses:
|
|
2563
|
+
'200':
|
|
2564
|
+
content:
|
|
2565
|
+
application/json:
|
|
2566
|
+
schema:
|
|
2567
|
+
$ref: '#/components/schemas/FieldQueryResponse'
|
|
2568
|
+
description: ''
|
|
2382
2569
|
/api/projects/projects/search/:
|
|
2383
2570
|
get:
|
|
2384
2571
|
operationId: projects_projects_search_retrieve
|
|
@@ -5814,6 +6001,51 @@ components:
|
|
|
5814
6001
|
required:
|
|
5815
6002
|
- items
|
|
5816
6003
|
- next_cursor
|
|
6004
|
+
FieldComparison:
|
|
6005
|
+
type: object
|
|
6006
|
+
properties:
|
|
6007
|
+
as_of:
|
|
6008
|
+
$ref: '#/components/schemas/FieldSnapshot'
|
|
6009
|
+
compare_to:
|
|
6010
|
+
$ref: '#/components/schemas/FieldSnapshot'
|
|
6011
|
+
changed:
|
|
6012
|
+
type: boolean
|
|
6013
|
+
nullable: true
|
|
6014
|
+
delta_days:
|
|
6015
|
+
type: integer
|
|
6016
|
+
nullable: true
|
|
6017
|
+
required:
|
|
6018
|
+
- as_of
|
|
6019
|
+
FieldDiscovery:
|
|
6020
|
+
type: object
|
|
6021
|
+
properties:
|
|
6022
|
+
timezone:
|
|
6023
|
+
type: string
|
|
6024
|
+
now:
|
|
6025
|
+
type: string
|
|
6026
|
+
format: date-time
|
|
6027
|
+
current_month_start:
|
|
6028
|
+
type: string
|
|
6029
|
+
format: date-time
|
|
6030
|
+
time_basis:
|
|
6031
|
+
type: string
|
|
6032
|
+
fields:
|
|
6033
|
+
type: array
|
|
6034
|
+
items:
|
|
6035
|
+
$ref: '#/components/schemas/QueryFieldDescriptor'
|
|
6036
|
+
total:
|
|
6037
|
+
type: integer
|
|
6038
|
+
next_offset:
|
|
6039
|
+
type: integer
|
|
6040
|
+
nullable: true
|
|
6041
|
+
required:
|
|
6042
|
+
- current_month_start
|
|
6043
|
+
- fields
|
|
6044
|
+
- next_offset
|
|
6045
|
+
- now
|
|
6046
|
+
- time_basis
|
|
6047
|
+
- timezone
|
|
6048
|
+
- total
|
|
5817
6049
|
FieldOption:
|
|
5818
6050
|
type: object
|
|
5819
6051
|
properties:
|
|
@@ -5886,6 +6118,112 @@ components:
|
|
|
5886
6118
|
- workflow_name
|
|
5887
6119
|
- written_by_email
|
|
5888
6120
|
- written_by_name
|
|
6121
|
+
FieldQueryCounts:
|
|
6122
|
+
type: object
|
|
6123
|
+
properties:
|
|
6124
|
+
candidate:
|
|
6125
|
+
type: integer
|
|
6126
|
+
matched:
|
|
6127
|
+
type: integer
|
|
6128
|
+
unevaluable:
|
|
6129
|
+
type: integer
|
|
6130
|
+
unmatched:
|
|
6131
|
+
type: integer
|
|
6132
|
+
matched_with_missing_history:
|
|
6133
|
+
type: integer
|
|
6134
|
+
required:
|
|
6135
|
+
- candidate
|
|
6136
|
+
- matched
|
|
6137
|
+
- matched_with_missing_history
|
|
6138
|
+
- unevaluable
|
|
6139
|
+
- unmatched
|
|
6140
|
+
FieldQueryItem:
|
|
6141
|
+
type: object
|
|
6142
|
+
properties:
|
|
6143
|
+
project_id:
|
|
6144
|
+
type: string
|
|
6145
|
+
format: uuid
|
|
6146
|
+
fields:
|
|
6147
|
+
type: object
|
|
6148
|
+
additionalProperties:
|
|
6149
|
+
$ref: '#/components/schemas/FieldComparison'
|
|
6150
|
+
required:
|
|
6151
|
+
- fields
|
|
6152
|
+
- project_id
|
|
6153
|
+
FieldQueryResponse:
|
|
6154
|
+
type: object
|
|
6155
|
+
properties:
|
|
6156
|
+
timezone:
|
|
6157
|
+
type: string
|
|
6158
|
+
time_basis:
|
|
6159
|
+
type: string
|
|
6160
|
+
as_of:
|
|
6161
|
+
type: string
|
|
6162
|
+
format: date-time
|
|
6163
|
+
compare_to:
|
|
6164
|
+
type: string
|
|
6165
|
+
format: date-time
|
|
6166
|
+
nullable: true
|
|
6167
|
+
query_clock:
|
|
6168
|
+
type: string
|
|
6169
|
+
format: date-time
|
|
6170
|
+
fields:
|
|
6171
|
+
type: array
|
|
6172
|
+
items:
|
|
6173
|
+
$ref: '#/components/schemas/QueryFieldDescriptor'
|
|
6174
|
+
counts:
|
|
6175
|
+
$ref: '#/components/schemas/FieldQueryCounts'
|
|
6176
|
+
items:
|
|
6177
|
+
type: array
|
|
6178
|
+
items:
|
|
6179
|
+
$ref: '#/components/schemas/FieldQueryItem'
|
|
6180
|
+
next_cursor:
|
|
6181
|
+
type: string
|
|
6182
|
+
nullable: true
|
|
6183
|
+
required:
|
|
6184
|
+
- as_of
|
|
6185
|
+
- compare_to
|
|
6186
|
+
- counts
|
|
6187
|
+
- fields
|
|
6188
|
+
- items
|
|
6189
|
+
- next_cursor
|
|
6190
|
+
- query_clock
|
|
6191
|
+
- time_basis
|
|
6192
|
+
- timezone
|
|
6193
|
+
FieldSnapshot:
|
|
6194
|
+
type: object
|
|
6195
|
+
properties:
|
|
6196
|
+
state:
|
|
6197
|
+
$ref: '#/components/schemas/FieldSnapshotStateEnum'
|
|
6198
|
+
value:
|
|
6199
|
+
nullable: true
|
|
6200
|
+
display:
|
|
6201
|
+
nullable: true
|
|
6202
|
+
write_id:
|
|
6203
|
+
type: string
|
|
6204
|
+
format: uuid
|
|
6205
|
+
recorded_at:
|
|
6206
|
+
type: string
|
|
6207
|
+
format: date-time
|
|
6208
|
+
source:
|
|
6209
|
+
type: string
|
|
6210
|
+
evidence_observation_ids:
|
|
6211
|
+
type: array
|
|
6212
|
+
items:
|
|
6213
|
+
type: string
|
|
6214
|
+
required:
|
|
6215
|
+
- state
|
|
6216
|
+
- value
|
|
6217
|
+
FieldSnapshotStateEnum:
|
|
6218
|
+
enum:
|
|
6219
|
+
- value
|
|
6220
|
+
- empty
|
|
6221
|
+
- missing
|
|
6222
|
+
type: string
|
|
6223
|
+
description: |-
|
|
6224
|
+
* `value` - value
|
|
6225
|
+
* `empty` - empty
|
|
6226
|
+
* `missing` - missing
|
|
5889
6227
|
FileDeleteRequestRequest:
|
|
5890
6228
|
type: object
|
|
5891
6229
|
properties:
|
|
@@ -7373,6 +7711,26 @@ components:
|
|
|
7373
7711
|
pattern: ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$
|
|
7374
7712
|
required:
|
|
7375
7713
|
- batch_id
|
|
7714
|
+
QueryFieldDescriptor:
|
|
7715
|
+
type: object
|
|
7716
|
+
properties:
|
|
7717
|
+
id:
|
|
7718
|
+
type: string
|
|
7719
|
+
format: uuid
|
|
7720
|
+
key:
|
|
7721
|
+
type: string
|
|
7722
|
+
label:
|
|
7723
|
+
type: string
|
|
7724
|
+
data_type:
|
|
7725
|
+
type: string
|
|
7726
|
+
is_array:
|
|
7727
|
+
type: boolean
|
|
7728
|
+
required:
|
|
7729
|
+
- data_type
|
|
7730
|
+
- id
|
|
7731
|
+
- is_array
|
|
7732
|
+
- key
|
|
7733
|
+
- label
|
|
7376
7734
|
QuickCapture:
|
|
7377
7735
|
type: object
|
|
7378
7736
|
description: |-
|
|
@@ -11,6 +11,11 @@ current login you@example.com # magic-link sign in, sto
|
|
|
11
11
|
current config set default_workspace <workspace-uuid> # used when --workspace is omitted
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
`current login` asks for the sign-in link from the email (or its token). An
|
|
15
|
+
account that requires two-factor authentication is also asked for an
|
|
16
|
+
authenticator code; one with no device enrolled yet is shown a setup link
|
|
17
|
+
and secret key to add to an authenticator app first.
|
|
18
|
+
|
|
14
19
|
The CLI speaks to `https://api.current.orinlabs.ai` by default. For another
|
|
15
20
|
server (for example local development), set
|
|
16
21
|
`current config set base_url http://localhost:8000`.
|
|
@@ -559,7 +564,8 @@ Options:
|
|
|
559
564
|
|
|
560
565
|
### current login <email>
|
|
561
566
|
|
|
562
|
-
Sign in with a magic link and
|
|
567
|
+
Sign in with a magic link (and an authenticator code when the account
|
|
568
|
+
requires two-factor authentication) and store the auth token.
|
|
563
569
|
|
|
564
570
|
### current logout
|
|
565
571
|
|
|
@@ -744,6 +750,17 @@ PATCH /api/projects/field-definitions/{id}/
|
|
|
744
750
|
Options:
|
|
745
751
|
- `--data` (required) - Request body as JSON: inline, @file.json, or - for stdin.
|
|
746
752
|
|
|
753
|
+
### current projects field-definitions query-metadata retrieve
|
|
754
|
+
|
|
755
|
+
Discover field keys, labels, types, and the workspace clock.
|
|
756
|
+
|
|
757
|
+
GET /api/projects/field-definitions/query-metadata/
|
|
758
|
+
|
|
759
|
+
Options:
|
|
760
|
+
- `--fields` - Optional comma-separated field keys
|
|
761
|
+
- `--offset`
|
|
762
|
+
- `--workspace` - Filter by workspace UUID
|
|
763
|
+
|
|
747
764
|
### current projects field-definitions retrieve <id>
|
|
748
765
|
|
|
749
766
|
Workspace-level custom field definitions. Only admins can mutate.
|
|
@@ -805,6 +822,15 @@ PATCH /api/projects/projects/{id}/
|
|
|
805
822
|
Options:
|
|
806
823
|
- `--data` (required) - Request body as JSON: inline, @file.json, or - for stdin.
|
|
807
824
|
|
|
825
|
+
### current projects query-fields create
|
|
826
|
+
|
|
827
|
+
Read canonical field snapshots. This POST does not write project state.
|
|
828
|
+
|
|
829
|
+
POST /api/projects/projects/query-fields/
|
|
830
|
+
|
|
831
|
+
Options:
|
|
832
|
+
- `--data` (required) - Request body as JSON: inline, @file.json, or - for stdin.
|
|
833
|
+
|
|
808
834
|
### current projects retrieve <id>
|
|
809
835
|
|
|
810
836
|
Projects across the caller's workspaces. Any member can create a
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""The login flow must match the API's magic-link contract."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from current_cli import config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_auth_handler(debug_token: str | None = "magic-1"):
|
|
11
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
12
|
+
if request.url.path == "/api/accounts/auth/magic-link/":
|
|
13
|
+
payload = {"detail": "Magic link sent."}
|
|
14
|
+
if debug_token:
|
|
15
|
+
payload["debug_token"] = debug_token
|
|
16
|
+
return httpx.Response(200, json=payload)
|
|
17
|
+
if request.url.path == "/api/accounts/auth/verify/":
|
|
18
|
+
body = json.loads(request.content)
|
|
19
|
+
if body["token"] != "magic-1":
|
|
20
|
+
return httpx.Response(400, json={"detail": "Invalid or expired link."})
|
|
21
|
+
return httpx.Response(
|
|
22
|
+
200, json={"token": "drf-token", "user": {"email": "dev@example.com"}}
|
|
23
|
+
)
|
|
24
|
+
if request.url.path == "/api/accounts/auth/me/":
|
|
25
|
+
return httpx.Response(200, json={"email": "dev@example.com"})
|
|
26
|
+
if request.url.path == "/api/accounts/auth/logout/":
|
|
27
|
+
return httpx.Response(200, json={"detail": "Successfully logged out."})
|
|
28
|
+
return httpx.Response(404, json={"detail": "Not found."})
|
|
29
|
+
|
|
30
|
+
return handler
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_login_with_debug_token(cli, runner, transport_factory):
|
|
34
|
+
transport = transport_factory(make_auth_handler())
|
|
35
|
+
|
|
36
|
+
result = runner.invoke(cli, ["login", "dev@example.com"])
|
|
37
|
+
|
|
38
|
+
assert result.exit_code == 0, result.output
|
|
39
|
+
assert config.get("token") == "drf-token"
|
|
40
|
+
assert config.get("email") == "dev@example.com"
|
|
41
|
+
magic, verify = transport.requests
|
|
42
|
+
assert json.loads(magic.content) == {"email": "dev@example.com"}
|
|
43
|
+
assert json.loads(verify.content) == {"token": "magic-1"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_login_prompts_without_debug_token(cli, runner, transport_factory):
|
|
47
|
+
transport_factory(make_auth_handler(debug_token=None))
|
|
48
|
+
|
|
49
|
+
result = runner.invoke(cli, ["login", "dev@example.com"], input="magic-1\n")
|
|
50
|
+
|
|
51
|
+
assert result.exit_code == 0, result.output
|
|
52
|
+
assert config.get("token") == "drf-token"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_login_bad_token_fails(cli, runner, transport_factory):
|
|
56
|
+
transport_factory(make_auth_handler(debug_token="wrong"))
|
|
57
|
+
|
|
58
|
+
result = runner.invoke(cli, ["login", "dev@example.com"])
|
|
59
|
+
|
|
60
|
+
assert result.exit_code == 1
|
|
61
|
+
assert config.get("token") is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_whoami_sends_token(cli, runner, transport_factory):
|
|
65
|
+
config.set_value("token", "drf-token")
|
|
66
|
+
transport = transport_factory(make_auth_handler())
|
|
67
|
+
|
|
68
|
+
result = runner.invoke(cli, ["whoami"])
|
|
69
|
+
|
|
70
|
+
assert result.exit_code == 0, result.output
|
|
71
|
+
assert transport.requests[0].headers["Authorization"] == "Token drf-token"
|
|
72
|
+
assert json.loads(result.output) == {"email": "dev@example.com"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_logout_clears_token(cli, runner, transport_factory):
|
|
76
|
+
config.set_value("token", "drf-token")
|
|
77
|
+
transport = transport_factory(make_auth_handler())
|
|
78
|
+
|
|
79
|
+
result = runner.invoke(cli, ["logout"])
|
|
80
|
+
|
|
81
|
+
assert result.exit_code == 0, result.output
|
|
82
|
+
assert config.get("token") is None
|
|
83
|
+
assert transport.requests[0].url.path == "/api/accounts/auth/logout/"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_config_set_and_get(cli, runner):
|
|
87
|
+
result = runner.invoke(cli, ["config", "set", "default_workspace", "ws-1"])
|
|
88
|
+
assert result.exit_code == 0, result.output
|
|
89
|
+
|
|
90
|
+
result = runner.invoke(cli, ["config", "get", "default_workspace"])
|
|
91
|
+
assert result.exit_code == 0
|
|
92
|
+
assert result.output.strip() == "ws-1"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def make_mfa_handler(*, enrolled: bool, code: str = "123456", ticket_valid: bool = True):
|
|
96
|
+
"""The API's MFA login branch: verify answers with a ticket instead of a
|
|
97
|
+
token, and a code (after enrollment, when not enrolled) completes it."""
|
|
98
|
+
|
|
99
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
100
|
+
path = request.url.path
|
|
101
|
+
body = json.loads(request.content) if request.content else {}
|
|
102
|
+
if path == "/api/accounts/auth/magic-link/":
|
|
103
|
+
return httpx.Response(200, json={"detail": "Magic link sent."})
|
|
104
|
+
if path == "/api/accounts/auth/verify/":
|
|
105
|
+
if body["token"] != "magic-1":
|
|
106
|
+
return httpx.Response(400, json={"detail": "Invalid or expired link."})
|
|
107
|
+
return httpx.Response(
|
|
108
|
+
200, json={"mfa_required": True, "mfa_enrolled": enrolled, "ticket": "ticket-1"}
|
|
109
|
+
)
|
|
110
|
+
if not ticket_valid or body.get("ticket") != "ticket-1":
|
|
111
|
+
status = 400 if path.endswith("/verify/") else 403
|
|
112
|
+
return httpx.Response(status, json={"detail": "Invalid or expired ticket."})
|
|
113
|
+
if path == "/api/accounts/auth/mfa/setup/":
|
|
114
|
+
return httpx.Response(
|
|
115
|
+
200,
|
|
116
|
+
json={"secret": "SECRET", "otpauth_url": "otpauth://totp/Current?secret=SECRET"},
|
|
117
|
+
)
|
|
118
|
+
if path in ("/api/accounts/auth/mfa/verify/", "/api/accounts/auth/mfa/confirm/"):
|
|
119
|
+
if body["code"] != code:
|
|
120
|
+
return httpx.Response(400, json={"detail": "Incorrect code."})
|
|
121
|
+
return httpx.Response(
|
|
122
|
+
200,
|
|
123
|
+
json={
|
|
124
|
+
"mfa_required": False,
|
|
125
|
+
"token": "drf-token",
|
|
126
|
+
"user": {"email": "a@orinlabs.ai"},
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
return httpx.Response(404, json={"detail": "Not found."})
|
|
130
|
+
|
|
131
|
+
return handler
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_login_with_enrolled_mfa(cli, runner, transport_factory):
|
|
135
|
+
transport = transport_factory(make_mfa_handler(enrolled=True))
|
|
136
|
+
|
|
137
|
+
result = runner.invoke(cli, ["login", "a@orinlabs.ai"], input="magic-1\n123 456\n")
|
|
138
|
+
|
|
139
|
+
assert result.exit_code == 0, result.output
|
|
140
|
+
assert config.get("token") == "drf-token"
|
|
141
|
+
paths = [r.url.path for r in transport.requests]
|
|
142
|
+
assert paths[-1] == "/api/accounts/auth/mfa/verify/"
|
|
143
|
+
assert json.loads(transport.requests[-1].content) == {"ticket": "ticket-1", "code": "123456"}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_login_enrolls_mfa_when_not_enrolled(cli, runner, transport_factory):
|
|
147
|
+
transport = transport_factory(make_mfa_handler(enrolled=False))
|
|
148
|
+
|
|
149
|
+
result = runner.invoke(cli, ["login", "a@orinlabs.ai"], input="magic-1\n123456\n")
|
|
150
|
+
|
|
151
|
+
assert result.exit_code == 0, result.output
|
|
152
|
+
assert "otpauth://totp/Current?secret=SECRET" in result.output
|
|
153
|
+
assert "SECRET" in result.output
|
|
154
|
+
assert config.get("token") == "drf-token"
|
|
155
|
+
paths = [r.url.path for r in transport.requests]
|
|
156
|
+
assert paths[-2:] == ["/api/accounts/auth/mfa/setup/", "/api/accounts/auth/mfa/confirm/"]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_login_retries_incorrect_code(cli, runner, transport_factory):
|
|
160
|
+
transport_factory(make_mfa_handler(enrolled=True))
|
|
161
|
+
|
|
162
|
+
result = runner.invoke(cli, ["login", "a@orinlabs.ai"], input="magic-1\n000000\n123456\n")
|
|
163
|
+
|
|
164
|
+
assert result.exit_code == 0, result.output
|
|
165
|
+
assert "Incorrect code. Try again." in result.output
|
|
166
|
+
assert config.get("token") == "drf-token"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_login_gives_up_after_three_incorrect_codes(cli, runner, transport_factory):
|
|
170
|
+
transport = transport_factory(make_mfa_handler(enrolled=True))
|
|
171
|
+
|
|
172
|
+
result = runner.invoke(
|
|
173
|
+
cli, ["login", "a@orinlabs.ai"], input="magic-1\n000000\n000000\n000000\n"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert result.exit_code == 1
|
|
177
|
+
assert config.get("token") is None
|
|
178
|
+
verifies = [r for r in transport.requests if r.url.path == "/api/accounts/auth/mfa/verify/"]
|
|
179
|
+
assert len(verifies) == 3
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_login_expired_ticket_says_to_start_again(cli, runner, transport_factory):
|
|
183
|
+
transport_factory(make_mfa_handler(enrolled=True, ticket_valid=False))
|
|
184
|
+
|
|
185
|
+
result = runner.invoke(cli, ["login", "a@orinlabs.ai"], input="magic-1\n123456\n")
|
|
186
|
+
|
|
187
|
+
assert result.exit_code == 1
|
|
188
|
+
assert "Run `current login` again." in result.output
|
|
189
|
+
assert config.get("token") is None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_login_expired_ticket_during_enrollment(cli, runner, transport_factory):
|
|
193
|
+
transport_factory(make_mfa_handler(enrolled=False, ticket_valid=False))
|
|
194
|
+
|
|
195
|
+
result = runner.invoke(cli, ["login", "a@orinlabs.ai"], input="magic-1\n")
|
|
196
|
+
|
|
197
|
+
assert result.exit_code == 1
|
|
198
|
+
assert "Run `current login` again." in result.output
|
|
199
|
+
assert config.get("token") is None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_login_accepts_pasted_sign_in_link(cli, runner, transport_factory):
|
|
203
|
+
transport = transport_factory(make_auth_handler(debug_token=None))
|
|
204
|
+
|
|
205
|
+
result = runner.invoke(
|
|
206
|
+
cli,
|
|
207
|
+
["login", "dev@example.com"],
|
|
208
|
+
input="https://current.orinlabs.ai/auth/verify?token=magic-1\n",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assert result.exit_code == 0, result.output
|
|
212
|
+
assert json.loads(transport.requests[1].content) == {"token": "magic-1"}
|
|
213
|
+
assert config.get("token") == "drf-token"
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""The login flow must match the API's magic-link contract."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
|
-
|
|
7
|
-
from current_cli import config
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def make_auth_handler(debug_token: str | None = "magic-1"):
|
|
11
|
-
def handler(request: httpx.Request) -> httpx.Response:
|
|
12
|
-
if request.url.path == "/api/accounts/auth/magic-link/":
|
|
13
|
-
payload = {"detail": "Magic link sent."}
|
|
14
|
-
if debug_token:
|
|
15
|
-
payload["debug_token"] = debug_token
|
|
16
|
-
return httpx.Response(200, json=payload)
|
|
17
|
-
if request.url.path == "/api/accounts/auth/verify/":
|
|
18
|
-
body = json.loads(request.content)
|
|
19
|
-
if body["token"] != "magic-1":
|
|
20
|
-
return httpx.Response(400, json={"detail": "Invalid or expired link."})
|
|
21
|
-
return httpx.Response(
|
|
22
|
-
200, json={"token": "drf-token", "user": {"email": "dev@example.com"}}
|
|
23
|
-
)
|
|
24
|
-
if request.url.path == "/api/accounts/auth/me/":
|
|
25
|
-
return httpx.Response(200, json={"email": "dev@example.com"})
|
|
26
|
-
if request.url.path == "/api/accounts/auth/logout/":
|
|
27
|
-
return httpx.Response(200, json={"detail": "Successfully logged out."})
|
|
28
|
-
return httpx.Response(404, json={"detail": "Not found."})
|
|
29
|
-
|
|
30
|
-
return handler
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_login_with_debug_token(cli, runner, transport_factory):
|
|
34
|
-
transport = transport_factory(make_auth_handler())
|
|
35
|
-
|
|
36
|
-
result = runner.invoke(cli, ["login", "dev@example.com"])
|
|
37
|
-
|
|
38
|
-
assert result.exit_code == 0, result.output
|
|
39
|
-
assert config.get("token") == "drf-token"
|
|
40
|
-
assert config.get("email") == "dev@example.com"
|
|
41
|
-
magic, verify = transport.requests
|
|
42
|
-
assert json.loads(magic.content) == {"email": "dev@example.com"}
|
|
43
|
-
assert json.loads(verify.content) == {"token": "magic-1"}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_login_prompts_without_debug_token(cli, runner, transport_factory):
|
|
47
|
-
transport_factory(make_auth_handler(debug_token=None))
|
|
48
|
-
|
|
49
|
-
result = runner.invoke(cli, ["login", "dev@example.com"], input="magic-1\n")
|
|
50
|
-
|
|
51
|
-
assert result.exit_code == 0, result.output
|
|
52
|
-
assert config.get("token") == "drf-token"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_login_bad_token_fails(cli, runner, transport_factory):
|
|
56
|
-
transport_factory(make_auth_handler(debug_token="wrong"))
|
|
57
|
-
|
|
58
|
-
result = runner.invoke(cli, ["login", "dev@example.com"])
|
|
59
|
-
|
|
60
|
-
assert result.exit_code == 1
|
|
61
|
-
assert config.get("token") is None
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_whoami_sends_token(cli, runner, transport_factory):
|
|
65
|
-
config.set_value("token", "drf-token")
|
|
66
|
-
transport = transport_factory(make_auth_handler())
|
|
67
|
-
|
|
68
|
-
result = runner.invoke(cli, ["whoami"])
|
|
69
|
-
|
|
70
|
-
assert result.exit_code == 0, result.output
|
|
71
|
-
assert transport.requests[0].headers["Authorization"] == "Token drf-token"
|
|
72
|
-
assert json.loads(result.output) == {"email": "dev@example.com"}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def test_logout_clears_token(cli, runner, transport_factory):
|
|
76
|
-
config.set_value("token", "drf-token")
|
|
77
|
-
transport = transport_factory(make_auth_handler())
|
|
78
|
-
|
|
79
|
-
result = runner.invoke(cli, ["logout"])
|
|
80
|
-
|
|
81
|
-
assert result.exit_code == 0, result.output
|
|
82
|
-
assert config.get("token") is None
|
|
83
|
-
assert transport.requests[0].url.path == "/api/accounts/auth/logout/"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_config_set_and_get(cli, runner):
|
|
87
|
-
result = runner.invoke(cli, ["config", "set", "default_workspace", "ws-1"])
|
|
88
|
-
assert result.exit_code == 0, result.output
|
|
89
|
-
|
|
90
|
-
result = runner.invoke(cli, ["config", "get", "default_workspace"])
|
|
91
|
-
assert result.exit_code == 0
|
|
92
|
-
assert result.output.strip() == "ws-1"
|
|
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
|
|
File without changes
|