datalab-python-sdk 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
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.
- {datalab_python_sdk-0.6.0.dist-info → datalab_python_sdk-0.7.0.dist-info}/METADATA +9 -1
- datalab_python_sdk-0.7.0.dist-info/RECORD +13 -0
- {datalab_python_sdk-0.6.0.dist-info → datalab_python_sdk-0.7.0.dist-info}/WHEEL +1 -1
- datalab_sdk/auth.py +130 -0
- datalab_sdk/cli.py +51 -0
- datalab_sdk/client.py +5 -1
- datalab_python_sdk-0.6.0.dist-info/RECORD +0 -12
- {datalab_python_sdk-0.6.0.dist-info → datalab_python_sdk-0.7.0.dist-info}/entry_points.txt +0 -0
- {datalab_python_sdk-0.6.0.dist-info → datalab_python_sdk-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datalab-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: SDK for the Datalab document intelligence API
|
|
5
5
|
Project-URL: Homepage, https://www.datalab.to
|
|
6
6
|
Project-URL: Documentation, https://documentation.datalab.to
|
|
@@ -41,6 +41,14 @@ pip install datalab-python-sdk
|
|
|
41
41
|
export DATALAB_API_KEY="your_api_key_here"
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Or sign in from a terminal, which also works for coding agents:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
datalab login
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It shows a code and a link. Open the link, sign in or sign up, approve the code, and type the 6-digit code that we email you. The SDK saves the key to `~/.config/datalab/config.json`, readable only by your user, and uses it when `DATALAB_API_KEY` is not set. `datalab logout` deletes it.
|
|
51
|
+
|
|
44
52
|
### Basic Usage
|
|
45
53
|
|
|
46
54
|
```python
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
datalab_sdk/__init__.py,sha256=BLAqVuqjxhdjKabkDUePLSmGeVMo0BnWfVbLDt8haP4,2397
|
|
2
|
+
datalab_sdk/auth.py,sha256=Z7eaKSxhDFt2uUbSIlU8yuOC2myDWXl-rY9JLuThg4A,4278
|
|
3
|
+
datalab_sdk/cli.py,sha256=icUy91brz0J8hzvC1G-pBtZPszc5oRd4H3sEpYhKr7Y,54186
|
|
4
|
+
datalab_sdk/client.py,sha256=aNPW4_gUdyW7UbLHI3YRzPA_Ae2dQmlQwCV38qsw0hA,125702
|
|
5
|
+
datalab_sdk/exceptions.py,sha256=5-lzlfvB7FKGOhvgyacceTFRRUxton_JqcIyuB8k_mU,2625
|
|
6
|
+
datalab_sdk/mimetypes.py,sha256=a4ULJmHxoR_RMsD0j51hx0t2Q7In_PzNm7UlWX07eEE,932
|
|
7
|
+
datalab_sdk/models.py,sha256=OpiIAdOVcL2tdfBkzRNHZ8MjGjRiBSjaKtFVf_U1l8w,32494
|
|
8
|
+
datalab_sdk/settings.py,sha256=B_B42QQADAz6ePXQTOqENZllcppFR4JpwonEk2vlzhw,693
|
|
9
|
+
datalab_python_sdk-0.7.0.dist-info/METADATA,sha256=z3hdjWxjWWjORewPlI58rO_stgNdHKUkwvf0Y4JemRg,6963
|
|
10
|
+
datalab_python_sdk-0.7.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
11
|
+
datalab_python_sdk-0.7.0.dist-info/entry_points.txt,sha256=rqWxO21UV57JxxyeF20iBTxHpzBYOA69hCHIqwdn-Ok,48
|
|
12
|
+
datalab_python_sdk-0.7.0.dist-info/licenses/LICENSE,sha256=XGjT-Uz6t7UZzr3JCZfilsGcZEgMm_1aONcRunohxWg,1064
|
|
13
|
+
datalab_python_sdk-0.7.0.dist-info/RECORD,,
|
datalab_sdk/auth.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Sign in from a terminal with a device code (`datalab login`), and keep the API key on disk.
|
|
2
|
+
|
|
3
|
+
The flow (server side: /api/auth/device/code, /app/device, /api/auth/device/token):
|
|
4
|
+
1. Ask the server for a code. Show the human the code and a URL, and open the browser.
|
|
5
|
+
2. The human approves in the browser, where they are signed in as usual.
|
|
6
|
+
3. Poll until the server returns a new API key, then save it to the config file.
|
|
7
|
+
|
|
8
|
+
The key is saved to $XDG_CONFIG_HOME/datalab/config.json (default ~/.config/datalab/config.json)
|
|
9
|
+
with mode 0600. DATALAB_API_KEY in the environment always wins over the saved key.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import stat
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
import webbrowser
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Callable, Optional
|
|
23
|
+
|
|
24
|
+
from datalab_sdk.exceptions import DatalabAPIError
|
|
25
|
+
|
|
26
|
+
KEYS_PAGE = "https://www.datalab.to/app/keys"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def config_path() -> Path:
|
|
30
|
+
base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(Path.home(), ".config")
|
|
31
|
+
return Path(base) / "datalab" / "config.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_saved_api_key() -> Optional[str]:
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(config_path().read_text()).get("api_key") or None
|
|
37
|
+
except (OSError, ValueError):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_api_key(api_key: str, host: str, team_name: Optional[str] = None) -> Path:
|
|
42
|
+
path = config_path()
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
path.write_text(
|
|
45
|
+
json.dumps({"api_key": api_key, "host": host, "team_name": team_name}, indent=2)
|
|
46
|
+
)
|
|
47
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def delete_saved_api_key() -> bool:
|
|
52
|
+
try:
|
|
53
|
+
config_path().unlink()
|
|
54
|
+
return True
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _post(url: str, payload: dict, headers: dict) -> tuple[int, dict]:
|
|
60
|
+
req = urllib.request.Request(
|
|
61
|
+
url,
|
|
62
|
+
data=json.dumps(payload).encode(),
|
|
63
|
+
headers={
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Accept": "application/json",
|
|
66
|
+
**headers,
|
|
67
|
+
},
|
|
68
|
+
method="POST",
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=30) as res:
|
|
72
|
+
return res.status, json.loads(res.read() or b"{}")
|
|
73
|
+
except urllib.error.HTTPError as e:
|
|
74
|
+
try:
|
|
75
|
+
return e.code, json.loads(e.read() or b"{}")
|
|
76
|
+
except ValueError:
|
|
77
|
+
return e.code, {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def device_login(
|
|
81
|
+
host: str = "https://www.datalab.to",
|
|
82
|
+
client_name: str = "Datalab CLI",
|
|
83
|
+
open_browser: bool = True,
|
|
84
|
+
extra_headers: Optional[dict] = None,
|
|
85
|
+
on_code: Optional[Callable[[dict], None]] = None,
|
|
86
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
87
|
+
) -> dict:
|
|
88
|
+
"""Run the device flow and return {"api_key", "team_name", "key_name"}. Raises DatalabAPIError."""
|
|
89
|
+
host = host.rstrip("/")
|
|
90
|
+
headers = extra_headers or {}
|
|
91
|
+
status, code = _post(
|
|
92
|
+
f"{host}/api/auth/device/code", {"client_name": client_name}, headers
|
|
93
|
+
)
|
|
94
|
+
if status != 200:
|
|
95
|
+
raise DatalabAPIError(
|
|
96
|
+
f"Could not start the sign-in ({status}): {code.get('detail', code)}"
|
|
97
|
+
)
|
|
98
|
+
if on_code:
|
|
99
|
+
on_code(code)
|
|
100
|
+
if open_browser:
|
|
101
|
+
try:
|
|
102
|
+
webbrowser.open(code["verification_uri_complete"])
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
interval = float(code.get("interval", 5))
|
|
107
|
+
deadline = time.monotonic() + float(code.get("expires_in", 600))
|
|
108
|
+
while time.monotonic() < deadline:
|
|
109
|
+
sleep(interval)
|
|
110
|
+
status, body = _post(
|
|
111
|
+
f"{host}/api/auth/device/token",
|
|
112
|
+
{"device_code": code["device_code"]},
|
|
113
|
+
headers,
|
|
114
|
+
)
|
|
115
|
+
if status == 200 and body.get("api_key"):
|
|
116
|
+
return body
|
|
117
|
+
error = body.get("error")
|
|
118
|
+
if error == "authorization_pending":
|
|
119
|
+
continue
|
|
120
|
+
if error == "slow_down":
|
|
121
|
+
interval += 5
|
|
122
|
+
continue
|
|
123
|
+
if error == "access_denied":
|
|
124
|
+
raise DatalabAPIError("The sign-in was denied in the browser.")
|
|
125
|
+
if error == "expired_token":
|
|
126
|
+
raise DatalabAPIError(
|
|
127
|
+
"The sign-in code expired. Run `datalab login` again."
|
|
128
|
+
)
|
|
129
|
+
raise DatalabAPIError(f"Sign-in failed ({status}): {body}")
|
|
130
|
+
raise DatalabAPIError("The sign-in code expired. Run `datalab login` again.")
|
datalab_sdk/cli.py
CHANGED
|
@@ -495,6 +495,55 @@ def cli():
|
|
|
495
495
|
pass
|
|
496
496
|
|
|
497
497
|
|
|
498
|
+
@click.command()
|
|
499
|
+
@click.option(
|
|
500
|
+
"--host",
|
|
501
|
+
default=None,
|
|
502
|
+
help="API host (default: DATALAB_HOST or https://www.datalab.to)",
|
|
503
|
+
)
|
|
504
|
+
@click.option(
|
|
505
|
+
"--no-browser",
|
|
506
|
+
is_flag=True,
|
|
507
|
+
help="Print the sign-in URL instead of opening a browser",
|
|
508
|
+
)
|
|
509
|
+
def login(host: Optional[str], no_browser: bool):
|
|
510
|
+
"""Sign in from this terminal and save an API key (device code)."""
|
|
511
|
+
from datalab_sdk.auth import device_login, save_api_key
|
|
512
|
+
|
|
513
|
+
host = (host or settings.DATALAB_HOST).rstrip("/")
|
|
514
|
+
|
|
515
|
+
def show(code: dict):
|
|
516
|
+
click.echo("To sign in, open this page and approve the code:")
|
|
517
|
+
click.echo(f" {code['verification_uri_complete']}")
|
|
518
|
+
click.echo(f" Code: {code['user_code']}")
|
|
519
|
+
click.echo("Then type the 6-digit code that we email you.")
|
|
520
|
+
click.echo("Waiting for approval...")
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
result = device_login(host=host, open_browser=not no_browser, on_code=show)
|
|
524
|
+
except DatalabError as e:
|
|
525
|
+
click.echo(f"Error: {e}", err=True)
|
|
526
|
+
sys.exit(1)
|
|
527
|
+
path = save_api_key(result["api_key"], host, result.get("team_name"))
|
|
528
|
+
click.echo(f"Signed in to team {result.get('team_name')}. API key saved to {path}.")
|
|
529
|
+
click.echo(
|
|
530
|
+
"DATALAB_API_KEY in your environment still takes priority over the saved key."
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@click.command()
|
|
535
|
+
def logout():
|
|
536
|
+
"""Delete the API key that `datalab login` saved. The key itself stays valid until you revoke it."""
|
|
537
|
+
from datalab_sdk.auth import config_path, delete_saved_api_key
|
|
538
|
+
|
|
539
|
+
if delete_saved_api_key():
|
|
540
|
+
click.echo(
|
|
541
|
+
f"Removed {config_path()}. Revoke the key at https://www.datalab.to/app/keys if you no longer need it."
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
click.echo("No saved API key.")
|
|
545
|
+
|
|
546
|
+
|
|
498
547
|
@click.command()
|
|
499
548
|
@click.argument("path", type=click.Path(exists=True))
|
|
500
549
|
@common_options
|
|
@@ -1584,6 +1633,8 @@ def _render_dag_simple(layers, children, step_map):
|
|
|
1584
1633
|
|
|
1585
1634
|
|
|
1586
1635
|
# Add commands to CLI group
|
|
1636
|
+
cli.add_command(login)
|
|
1637
|
+
cli.add_command(logout)
|
|
1587
1638
|
cli.add_command(convert)
|
|
1588
1639
|
cli.add_command(extract)
|
|
1589
1640
|
cli.add_command(segment)
|
datalab_sdk/client.py
CHANGED
|
@@ -201,9 +201,13 @@ class AsyncDatalabClient:
|
|
|
201
201
|
"""
|
|
202
202
|
if api_key is None:
|
|
203
203
|
api_key = settings.DATALAB_API_KEY
|
|
204
|
+
if api_key is None:
|
|
205
|
+
from datalab_sdk.auth import load_saved_api_key
|
|
206
|
+
|
|
207
|
+
api_key = load_saved_api_key()
|
|
204
208
|
if api_key is None:
|
|
205
209
|
raise DatalabAPIError(
|
|
206
|
-
"
|
|
210
|
+
"No API key. Run `datalab login`, set DATALAB_API_KEY, or pass api_key. "
|
|
207
211
|
"Get a free API key at https://www.datalab.to/app/keys"
|
|
208
212
|
)
|
|
209
213
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
datalab_sdk/__init__.py,sha256=BLAqVuqjxhdjKabkDUePLSmGeVMo0BnWfVbLDt8haP4,2397
|
|
2
|
-
datalab_sdk/cli.py,sha256=DAI7fzs_1cqEctp9N_rw9VPuiIFHgN75w41FG55HxrM,52427
|
|
3
|
-
datalab_sdk/client.py,sha256=PvcNA7ff6yqIKvLkspG2XzSYMStYKlvAIkZRNDNblWg,125551
|
|
4
|
-
datalab_sdk/exceptions.py,sha256=5-lzlfvB7FKGOhvgyacceTFRRUxton_JqcIyuB8k_mU,2625
|
|
5
|
-
datalab_sdk/mimetypes.py,sha256=a4ULJmHxoR_RMsD0j51hx0t2Q7In_PzNm7UlWX07eEE,932
|
|
6
|
-
datalab_sdk/models.py,sha256=OpiIAdOVcL2tdfBkzRNHZ8MjGjRiBSjaKtFVf_U1l8w,32494
|
|
7
|
-
datalab_sdk/settings.py,sha256=B_B42QQADAz6ePXQTOqENZllcppFR4JpwonEk2vlzhw,693
|
|
8
|
-
datalab_python_sdk-0.6.0.dist-info/METADATA,sha256=E2qTxFzvZarV8CeVwvGR7UW-EA6lUn_iLG9NlIT450s,6582
|
|
9
|
-
datalab_python_sdk-0.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
-
datalab_python_sdk-0.6.0.dist-info/entry_points.txt,sha256=rqWxO21UV57JxxyeF20iBTxHpzBYOA69hCHIqwdn-Ok,48
|
|
11
|
-
datalab_python_sdk-0.6.0.dist-info/licenses/LICENSE,sha256=XGjT-Uz6t7UZzr3JCZfilsGcZEgMm_1aONcRunohxWg,1064
|
|
12
|
-
datalab_python_sdk-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|