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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datalab-python-sdk
3
- Version: 0.6.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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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
- "You must pass in an api_key or set DATALAB_API_KEY. "
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,,