datalab-python-sdk 0.6.0__tar.gz → 0.7.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.
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/CHANGELOG.md +11 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/PKG-INFO +9 -1
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/README.md +8 -0
- datalab_python_sdk-0.7.0/datalab_sdk/auth.py +130 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/cli.py +51 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/client.py +5 -1
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/pyproject.toml +1 -1
- datalab_python_sdk-0.7.0/tests/test_device_login.py +143 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/uv.lock +1 -1
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.github/workflows/ci.yml +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.gitignore +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.pre-commit-config.yaml +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.python-version +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/LICENSE +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/08-Lambda-Calculus.pptx +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/adversarial.pdf +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/bid_evaluation.docx +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/book_review.ppt +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/book_store.xls +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/chi_hind.png +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/how_to_read.doc +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/normandy.epub +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/sample-1-sheet.xlsx +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/thinkpython.pdf +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/vibe.html +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/__init__.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/exceptions.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/mimetypes.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/models.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/settings.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/README.md +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/__init__.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/test_live_api.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/test_readme_examples.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/pytest.ini +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/README.md +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/end_to_end_workflow.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/1_get_step_types.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/2_get_workflows.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/3_create_workflow.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/4_execute_workflow.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/5_poll_workflow.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/README.md +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/README.md +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/eval_segmentation.json +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/parse_segment.json +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/segment_parallel_extract.json +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/slack_alert.json +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/__init__.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/conftest.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_api_consistency.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_cli_options.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_cli_simple.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_client_methods.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_errors.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_new_endpoints.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_results.py +0 -0
- {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_workflows.py +0 -0
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
The newest release is at the top. Each export to this repository uses the
|
|
4
4
|
top section as its commit message.
|
|
5
5
|
|
|
6
|
+
## 0.7.0
|
|
7
|
+
|
|
8
|
+
New:
|
|
9
|
+
|
|
10
|
+
- `datalab login`: sign in from a terminal or a coding agent with a device
|
|
11
|
+
code. Approve it in the browser, type the emailed 6-digit code, and the SDK
|
|
12
|
+
saves an API key to `~/.config/datalab/config.json` (mode 0600).
|
|
13
|
+
`datalab logout` deletes it.
|
|
14
|
+
- The clients use the saved key when no `api_key` is passed and
|
|
15
|
+
`DATALAB_API_KEY` is not set.
|
|
16
|
+
|
|
6
17
|
## 0.6.0
|
|
7
18
|
|
|
8
19
|
Breaking changes:
|
|
@@ -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
|
|
@@ -20,6 +20,14 @@ pip install datalab-python-sdk
|
|
|
20
20
|
export DATALAB_API_KEY="your_api_key_here"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Or sign in from a terminal, which also works for coding agents:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
datalab login
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
23
31
|
### Basic Usage
|
|
24
32
|
|
|
25
33
|
```python
|
|
@@ -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.")
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -7,7 +7,7 @@ readme = "README.md"
|
|
|
7
7
|
license = "MIT"
|
|
8
8
|
repository = "https://github.com/datalab-to/sdk"
|
|
9
9
|
keywords = ["datalab", "sdk", "document-intelligence", "api"]
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.7.0"
|
|
11
11
|
description = "SDK for the Datalab document intelligence API"
|
|
12
12
|
requires-python = ">=3.11"
|
|
13
13
|
dependencies = [
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""`datalab login` (device code) against a small fake server, plus the saved-key fallback."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from click.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from datalab_sdk import auth
|
|
11
|
+
from datalab_sdk.exceptions import DatalabAPIError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeServer:
|
|
15
|
+
"""Answers /api/auth/device/code once, then `pending` polls, then the final reply."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, pending_polls=1, final=None):
|
|
18
|
+
self.pending_polls = pending_polls
|
|
19
|
+
self.final = final or (
|
|
20
|
+
200,
|
|
21
|
+
{"api_key": "key_123", "team_name": "Acme", "key_name": "Datalab CLI"},
|
|
22
|
+
)
|
|
23
|
+
self.polls = 0
|
|
24
|
+
outer = self
|
|
25
|
+
|
|
26
|
+
class Handler(BaseHTTPRequestHandler):
|
|
27
|
+
def log_message(self, *args):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def _send(self, status, body):
|
|
31
|
+
data = json.dumps(body).encode()
|
|
32
|
+
self.send_response(status)
|
|
33
|
+
self.send_header("Content-Type", "application/json")
|
|
34
|
+
self.send_header("Content-Length", str(len(data)))
|
|
35
|
+
self.end_headers()
|
|
36
|
+
self.wfile.write(data)
|
|
37
|
+
|
|
38
|
+
def do_POST(self):
|
|
39
|
+
self.rfile.read(int(self.headers.get("Content-Length") or 0))
|
|
40
|
+
if self.path == "/api/auth/device/code":
|
|
41
|
+
host = f"http://127.0.0.1:{outer.port}"
|
|
42
|
+
return self._send(
|
|
43
|
+
200,
|
|
44
|
+
{
|
|
45
|
+
"device_code": "dc",
|
|
46
|
+
"user_code": "BCDF-GHJK",
|
|
47
|
+
"verification_uri": f"{host}/app/device",
|
|
48
|
+
"verification_uri_complete": f"{host}/app/device?code=BCDF-GHJK",
|
|
49
|
+
"expires_in": 600,
|
|
50
|
+
"interval": 0,
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
outer.polls += 1
|
|
54
|
+
if outer.polls <= outer.pending_polls:
|
|
55
|
+
return self._send(400, {"error": "authorization_pending"})
|
|
56
|
+
return self._send(*outer.final)
|
|
57
|
+
|
|
58
|
+
self.httpd = HTTPServer(("127.0.0.1", 0), Handler)
|
|
59
|
+
self.port = self.httpd.server_address[1]
|
|
60
|
+
self.url = f"http://127.0.0.1:{self.port}"
|
|
61
|
+
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
|
|
62
|
+
|
|
63
|
+
def close(self):
|
|
64
|
+
self.httpd.shutdown()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def config_home(tmp_path, monkeypatch):
|
|
69
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
70
|
+
return tmp_path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_device_login_returns_key_after_pending():
|
|
74
|
+
server = FakeServer(pending_polls=2)
|
|
75
|
+
try:
|
|
76
|
+
shown = []
|
|
77
|
+
result = auth.device_login(
|
|
78
|
+
host=server.url,
|
|
79
|
+
open_browser=False,
|
|
80
|
+
on_code=shown.append,
|
|
81
|
+
sleep=lambda s: None,
|
|
82
|
+
)
|
|
83
|
+
finally:
|
|
84
|
+
server.close()
|
|
85
|
+
assert result["api_key"] == "key_123"
|
|
86
|
+
assert shown[0]["user_code"] == "BCDF-GHJK"
|
|
87
|
+
assert server.polls == 3
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize(
|
|
91
|
+
"error,message", [("access_denied", "denied"), ("expired_token", "expired")]
|
|
92
|
+
)
|
|
93
|
+
def test_device_login_errors(error, message):
|
|
94
|
+
server = FakeServer(pending_polls=0, final=(400, {"error": error}))
|
|
95
|
+
try:
|
|
96
|
+
with pytest.raises(DatalabAPIError, match=message):
|
|
97
|
+
auth.device_login(host=server.url, open_browser=False, sleep=lambda s: None)
|
|
98
|
+
finally:
|
|
99
|
+
server.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_save_load_delete_key(config_home):
|
|
103
|
+
path = auth.save_api_key("key_abc", "https://www.datalab.to", "Acme")
|
|
104
|
+
assert path == config_home / "datalab" / "config.json"
|
|
105
|
+
assert oct(path.stat().st_mode & 0o777) == "0o600"
|
|
106
|
+
assert auth.load_saved_api_key() == "key_abc"
|
|
107
|
+
assert auth.delete_saved_api_key() is True
|
|
108
|
+
assert auth.load_saved_api_key() is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_client_falls_back_to_saved_key(config_home, monkeypatch):
|
|
112
|
+
from datalab_sdk.client import DatalabClient
|
|
113
|
+
from datalab_sdk.settings import settings
|
|
114
|
+
|
|
115
|
+
monkeypatch.setattr(settings, "DATALAB_API_KEY", None)
|
|
116
|
+
auth.save_api_key("key_saved", "https://www.datalab.to")
|
|
117
|
+
assert DatalabClient()._async_client.api_key == "key_saved"
|
|
118
|
+
monkeypatch.setattr(settings, "DATALAB_API_KEY", "key_env")
|
|
119
|
+
assert DatalabClient()._async_client.api_key == "key_env"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_client_error_names_login_and_keys_page(config_home, monkeypatch):
|
|
123
|
+
from datalab_sdk.client import DatalabClient
|
|
124
|
+
from datalab_sdk.settings import settings
|
|
125
|
+
|
|
126
|
+
monkeypatch.setattr(settings, "DATALAB_API_KEY", None)
|
|
127
|
+
with pytest.raises(DatalabAPIError, match="datalab login"):
|
|
128
|
+
DatalabClient()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_cli_login_saves_key(config_home):
|
|
132
|
+
from datalab_sdk.cli import cli
|
|
133
|
+
|
|
134
|
+
server = FakeServer(pending_polls=0)
|
|
135
|
+
try:
|
|
136
|
+
res = CliRunner().invoke(cli, ["login", "--host", server.url, "--no-browser"])
|
|
137
|
+
finally:
|
|
138
|
+
server.close()
|
|
139
|
+
assert res.exit_code == 0, res.output
|
|
140
|
+
assert "BCDF-GHJK" in res.output and "Signed in to team Acme" in res.output
|
|
141
|
+
assert auth.load_saved_api_key() == "key_123"
|
|
142
|
+
out = CliRunner().invoke(cli, ["logout"])
|
|
143
|
+
assert out.exit_code == 0 and auth.load_saved_api_key() is None
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/end_to_end_workflow.py
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
|
|
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
|