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.
Files changed (58) hide show
  1. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/CHANGELOG.md +11 -0
  2. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/PKG-INFO +9 -1
  3. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/README.md +8 -0
  4. datalab_python_sdk-0.7.0/datalab_sdk/auth.py +130 -0
  5. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/cli.py +51 -0
  6. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/client.py +5 -1
  7. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/pyproject.toml +1 -1
  8. datalab_python_sdk-0.7.0/tests/test_device_login.py +143 -0
  9. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/uv.lock +1 -1
  10. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.github/workflows/ci.yml +0 -0
  11. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.gitignore +0 -0
  12. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.pre-commit-config.yaml +0 -0
  13. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/.python-version +0 -0
  14. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/LICENSE +0 -0
  15. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/08-Lambda-Calculus.pptx +0 -0
  16. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/adversarial.pdf +0 -0
  17. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/bid_evaluation.docx +0 -0
  18. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/book_review.ppt +0 -0
  19. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/book_store.xls +0 -0
  20. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/chi_hind.png +0 -0
  21. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/how_to_read.doc +0 -0
  22. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/normandy.epub +0 -0
  23. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/sample-1-sheet.xlsx +0 -0
  24. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/thinkpython.pdf +0 -0
  25. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/data/vibe.html +0 -0
  26. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/__init__.py +0 -0
  27. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/exceptions.py +0 -0
  28. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/mimetypes.py +0 -0
  29. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/models.py +0 -0
  30. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/datalab_sdk/settings.py +0 -0
  31. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/README.md +0 -0
  32. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/__init__.py +0 -0
  33. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/test_live_api.py +0 -0
  34. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/integration/test_readme_examples.py +0 -0
  35. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/pytest.ini +0 -0
  36. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/README.md +0 -0
  37. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/end_to_end_workflow.py +0 -0
  38. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/1_get_step_types.py +0 -0
  39. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/2_get_workflows.py +0 -0
  40. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/3_create_workflow.py +0 -0
  41. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/4_execute_workflow.py +0 -0
  42. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/5_poll_workflow.py +0 -0
  43. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_api_tutorial/README.md +0 -0
  44. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/README.md +0 -0
  45. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/eval_segmentation.json +0 -0
  46. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/parse_segment.json +0 -0
  47. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/segment_parallel_extract.json +0 -0
  48. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/recipes/workflows/workflow_definitions/slack_alert.json +0 -0
  49. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/__init__.py +0 -0
  50. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/conftest.py +0 -0
  51. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_api_consistency.py +0 -0
  52. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_cli_options.py +0 -0
  53. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_cli_simple.py +0 -0
  54. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_client_methods.py +0 -0
  55. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_errors.py +0 -0
  56. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_new_endpoints.py +0 -0
  57. {datalab_python_sdk-0.6.0 → datalab_python_sdk-0.7.0}/tests/test_results.py +0 -0
  58. {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.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
@@ -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
- "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
 
@@ -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.6.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
@@ -234,7 +234,7 @@ wheels = [
234
234
 
235
235
  [[package]]
236
236
  name = "datalab-python-sdk"
237
- version = "0.6.0"
237
+ version = "0.7.0"
238
238
  source = { editable = "." }
239
239
  dependencies = [
240
240
  { name = "aiohttp" },