datalegion-cli 1.3.0__tar.gz → 1.3.1__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.
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/PKG-INFO +2 -2
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/README.md +1 -1
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/pyproject.toml +1 -1
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/config.py +1 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/login.py +10 -5
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/main.py +9 -2
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_login.py +30 -6
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/.gitignore +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/__init__.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/async_typer.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/client.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/commands/__init__.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/commands/company.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/commands/credits.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/commands/person.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/commands/utility.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/logging.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/output.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/src/datalegion_cli/update.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/__init__.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/conftest.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_client.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_commands.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_config.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_logging.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_output.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_package_contents.py +0 -0
- {datalegion_cli-1.3.0 → datalegion_cli-1.3.1}/tests/test_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datalegion-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: CLI for the Data Legion API — agent-friendly, fully async
|
|
5
5
|
Project-URL: Homepage, https://www.datalegion.ai
|
|
6
6
|
Project-URL: Documentation, https://www.datalegion.ai/docs
|
|
@@ -104,7 +104,7 @@ datalegion-cli login --team-id 7 # when your account has several teams
|
|
|
104
104
|
datalegion-cli login --no-browser # headless: prints the link to open on another device
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
The key belongs to this machine
|
|
107
|
+
The key belongs to this machine, named with a random id (`cli-` plus 8 hex characters) that the CLI generates on first login and keeps in its config, so nothing about the machine (such as its hostname) is sent. Pass `--device <name>` for a readable label instead. Signing in again replaces it, and you can disconnect it from Connected Apps in the dashboard.
|
|
108
108
|
|
|
109
109
|
Or set an API key yourself (stored encrypted on disk):
|
|
110
110
|
|
|
@@ -70,7 +70,7 @@ datalegion-cli login --team-id 7 # when your account has several teams
|
|
|
70
70
|
datalegion-cli login --no-browser # headless: prints the link to open on another device
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
The key belongs to this machine
|
|
73
|
+
The key belongs to this machine, named with a random id (`cli-` plus 8 hex characters) that the CLI generates on first login and keeps in its config, so nothing about the machine (such as its hostname) is sent. Pass `--device <name>` for a readable label instead. Signing in again replaces it, and you can disconnect it from Connected Apps in the dashboard.
|
|
74
74
|
|
|
75
75
|
Or set an API key yourself (stored encrypted on disk):
|
|
76
76
|
|
|
@@ -27,6 +27,7 @@ _DEFAULTS: dict[str, object] = {
|
|
|
27
27
|
"api_key": "",
|
|
28
28
|
"default_output": "json", # json | table | raw
|
|
29
29
|
"install_method": "", # brew | github | git | pip
|
|
30
|
+
"device_id": "", # random name for this machine's login key (see login.new_device_id)
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
# Fields that are stored encrypted on disk
|
|
@@ -12,8 +12,8 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import os
|
|
15
|
-
import platform
|
|
16
15
|
import re
|
|
16
|
+
import secrets
|
|
17
17
|
from typing import TYPE_CHECKING, Any
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
@@ -40,10 +40,15 @@ class LoginError(Exception):
|
|
|
40
40
|
self.details = details
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
def new_device_id() -> str:
|
|
44
|
+
"""A random name for this machine's key: ``cli-`` plus 8 hex characters.
|
|
45
|
+
|
|
46
|
+
Random rather than the hostname (or a hash of it): hostnames often carry a
|
|
47
|
+
person's name and are guessable, so nothing about the machine leaves it. Saved
|
|
48
|
+
in the config on first login and reused, so signing in again replaces this
|
|
49
|
+
machine's key instead of adding another.
|
|
50
|
+
"""
|
|
51
|
+
return f"cli-{secrets.token_hex(4)}"
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
def check_device_name(name: str) -> str:
|
|
@@ -126,7 +126,10 @@ async def login(
|
|
|
126
126
|
int | None, typer.Option("--team-id", help="Team the key bills, when you have several.")
|
|
127
127
|
] = None,
|
|
128
128
|
device: Annotated[
|
|
129
|
-
str | None,
|
|
129
|
+
str | None,
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--device", help="Name for this machine (default: a random id saved locally)."
|
|
132
|
+
),
|
|
130
133
|
] = None,
|
|
131
134
|
no_browser: Annotated[
|
|
132
135
|
bool, typer.Option("--no-browser", help="Print the sign-in link without opening it.")
|
|
@@ -145,7 +148,11 @@ async def login(
|
|
|
145
148
|
from datalegion_cli import login as flow
|
|
146
149
|
|
|
147
150
|
try:
|
|
148
|
-
|
|
151
|
+
cfg = load_config()
|
|
152
|
+
if not device and not cfg.get("device_id"):
|
|
153
|
+
cfg["device_id"] = flow.new_device_id()
|
|
154
|
+
save_config(cfg)
|
|
155
|
+
device_name = flow.check_device_name(device or str(cfg["device_id"]))
|
|
149
156
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
150
157
|
started = await flow.start_device_authorization(client)
|
|
151
158
|
link = started.get("verification_uri_complete") or started["verification_uri"]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import re
|
|
6
7
|
from unittest.mock import AsyncMock, patch
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
@@ -32,13 +33,15 @@ def tmp_config(tmp_config_dir, monkeypatch):
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class TestDeviceName:
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
assert
|
|
36
|
+
def test_new_ids_are_random_and_api_safe(self):
|
|
37
|
+
ids = {flow.new_device_id() for _ in range(50)}
|
|
38
|
+
assert len(ids) == 50
|
|
39
|
+
for device_id in ids:
|
|
40
|
+
assert re.fullmatch(r"cli-[0-9a-f]{8}", device_id)
|
|
41
|
+
assert flow.check_device_name(device_id) == device_id
|
|
38
42
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
assert flow.default_device_name() == "cli"
|
|
43
|
+
def test_the_hostname_is_never_read(self):
|
|
44
|
+
assert not hasattr(flow, "platform")
|
|
42
45
|
|
|
43
46
|
@pytest.mark.parametrize("name", ["", " lead", "a\nb", "x" * 101, "semi;colon", "two spaces"])
|
|
44
47
|
def test_rejects_what_the_api_rejects(self, name):
|
|
@@ -181,6 +184,27 @@ class TestLoginCommand:
|
|
|
181
184
|
|
|
182
185
|
assert load_config()["api_key"] == KEY
|
|
183
186
|
|
|
187
|
+
def test_default_device_is_a_saved_random_id_reused_next_time(self, steps, tmp_config):
|
|
188
|
+
exchange, _ = steps
|
|
189
|
+
assert runner.invoke(app, ["login"]).exit_code == 0
|
|
190
|
+
first = exchange.await_args.args[3]
|
|
191
|
+
assert re.fullmatch(r"cli-[0-9a-f]{8}", first)
|
|
192
|
+
from datalegion_cli.config import load_config
|
|
193
|
+
|
|
194
|
+
assert load_config()["device_id"] == first
|
|
195
|
+
assert runner.invoke(app, ["login"]).exit_code == 0
|
|
196
|
+
assert exchange.await_args.args[3] == first # same machine, same key slot
|
|
197
|
+
|
|
198
|
+
def test_device_flag_wins_and_leaves_the_saved_id(self, steps, tmp_config):
|
|
199
|
+
exchange, _ = steps
|
|
200
|
+
assert runner.invoke(app, ["login"]).exit_code == 0
|
|
201
|
+
from datalegion_cli.config import load_config
|
|
202
|
+
|
|
203
|
+
saved = load_config()["device_id"]
|
|
204
|
+
assert runner.invoke(app, ["login", "--device", "laptop"]).exit_code == 0
|
|
205
|
+
assert exchange.await_args.args[3] == "laptop"
|
|
206
|
+
assert load_config()["device_id"] == saved
|
|
207
|
+
|
|
184
208
|
def test_no_browser(self, steps, tmp_config):
|
|
185
209
|
_, browser = steps
|
|
186
210
|
assert runner.invoke(app, ["login", "--device", "laptop", "--no-browser"]).exit_code == 0
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|