datalegion-cli 1.4.0__tar.gz → 1.5.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 (28) hide show
  1. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/PKG-INFO +10 -1
  2. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/README.md +9 -0
  3. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/pyproject.toml +1 -1
  4. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/client.py +13 -1
  5. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/config.py +8 -0
  6. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/main.py +11 -0
  7. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_client.py +44 -0
  8. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/.gitignore +0 -0
  9. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/__init__.py +0 -0
  10. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/async_typer.py +0 -0
  11. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/commands/__init__.py +0 -0
  12. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/commands/company.py +0 -0
  13. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/commands/credits.py +0 -0
  14. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/commands/person.py +0 -0
  15. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/commands/utility.py +0 -0
  16. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/logging.py +0 -0
  17. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/login.py +0 -0
  18. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/output.py +0 -0
  19. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/src/datalegion_cli/update.py +0 -0
  20. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/__init__.py +0 -0
  21. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/conftest.py +0 -0
  22. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_commands.py +0 -0
  23. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_config.py +0 -0
  24. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_logging.py +0 -0
  25. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_login.py +0 -0
  26. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_output.py +0 -0
  27. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_package_contents.py +0 -0
  28. {datalegion_cli-1.4.0 → datalegion_cli-1.5.0}/tests/test_update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datalegion-cli
3
- Version: 1.4.0
3
+ Version: 1.5.0
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
@@ -271,12 +271,21 @@ datalegion-cli person enrich --email bad 2>/dev/null
271
271
  # {"ok": false, "error": "bad_request", "message": "..."}
272
272
  ```
273
273
 
274
+ ## Sandbox
275
+
276
+ Try commands without spending credits: `--sandbox` (or `LEGION_SANDBOX=1`, or `datalegion-cli config set sandbox true`) makes enrich, bulk enrich, search and discover return sample records with every field a real record has. Utilities, which are free, still call the real API. Sandbox discover answers only a few fixed example queries.
277
+
278
+ ```bash
279
+ datalegion-cli --sandbox person enrich --email anyone@example.com
280
+ ```
281
+
274
282
  ## Global flags
275
283
 
276
284
  | Flag | Description |
277
285
  |------|-------------|
278
286
  | `--verbose` / `-v` | Enable debug logging to stderr |
279
287
  | `--quiet` / `-q` | Suppress all log output |
288
+ | `--sandbox` | Return sample records from enrich, search and discover, using no credits |
280
289
  | `--help` | Show help for any command |
281
290
 
282
291
  ## Development
@@ -236,12 +236,21 @@ datalegion-cli person enrich --email bad 2>/dev/null
236
236
  # {"ok": false, "error": "bad_request", "message": "..."}
237
237
  ```
238
238
 
239
+ ## Sandbox
240
+
241
+ Try commands without spending credits: `--sandbox` (or `LEGION_SANDBOX=1`, or `datalegion-cli config set sandbox true`) makes enrich, bulk enrich, search and discover return sample records with every field a real record has. Utilities, which are free, still call the real API. Sandbox discover answers only a few fixed example queries.
242
+
243
+ ```bash
244
+ datalegion-cli --sandbox person enrich --email anyone@example.com
245
+ ```
246
+
239
247
  ## Global flags
240
248
 
241
249
  | Flag | Description |
242
250
  |------|-------------|
243
251
  | `--verbose` / `-v` | Enable debug logging to stderr |
244
252
  | `--quiet` / `-q` | Suppress all log output |
253
+ | `--sandbox` | Return sample records from enrich, search and discover, using no credits |
245
254
  | `--help` | Show help for any command |
246
255
 
247
256
  ## Development
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datalegion-cli"
3
- version = "1.4.0"
3
+ version = "1.5.0"
4
4
  description = "CLI for the Data Legion API — agent-friendly, fully async"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -8,7 +8,7 @@ import httpx
8
8
  from loguru import logger
9
9
 
10
10
  from datalegion_cli import __version__
11
- from datalegion_cli.config import get_api_key, get_base_url
11
+ from datalegion_cli.config import get_api_key, get_base_url, get_sandbox
12
12
 
13
13
 
14
14
  class APIError(Exception):
@@ -32,6 +32,15 @@ class APIError(Exception):
32
32
  return d
33
33
 
34
34
 
35
+ # Endpoints the API mirrors under /sandbox/: sample records (Jane Sandbox, Sandbox Corp), no credits.
36
+ # Utilities, reports, credits and dynamic bulk have no sandbox and call the real API.
37
+ SANDBOX_PATHS = frozenset(
38
+ f"/{entity}/{op}"
39
+ for entity in ("person", "company")
40
+ for op in ("enrich", "enrich/bulk", "search", "discover")
41
+ )
42
+
43
+
35
44
  class LegionClient:
36
45
  """Async client wrapping the Data Legion API."""
37
46
 
@@ -45,6 +54,7 @@ class LegionClient:
45
54
  self._base_url = base_url or get_base_url()
46
55
  self._api_key = (api_key or "").strip() or get_api_key()
47
56
  self._timeout = timeout
57
+ self._sandbox = get_sandbox()
48
58
  self._client: httpx.AsyncClient | None = None
49
59
  self.last_credits_used: str | None = None
50
60
  self.last_credits_remaining: str | None = None
@@ -77,6 +87,8 @@ class LegionClient:
77
87
  params: dict[str, Any] | None = None,
78
88
  ) -> dict[str, Any] | list[Any]:
79
89
  client = await self._get_client()
90
+ if self._sandbox and endpoint in SANDBOX_PATHS:
91
+ endpoint = f"/sandbox{endpoint}"
80
92
  logger.debug("{} {}", method.upper(), endpoint)
81
93
 
82
94
  if method.upper() == "GET":
@@ -28,6 +28,7 @@ _DEFAULTS: dict[str, object] = {
28
28
  "default_output": "json", # json | table | raw
29
29
  "install_method": "", # brew | github | git | pip
30
30
  "device_id": "", # random name for this machine's login key (see login.new_device_id)
31
+ "sandbox": False, # sample records, no credits (see client.SANDBOX_PATHS)
31
32
  }
32
33
 
33
34
  # Fields that are stored encrypted on disk
@@ -94,6 +95,8 @@ def load_config() -> dict[str, object]:
94
95
  cfg["api_key"] = env_key
95
96
  if env_url := os.environ.get("LEGION_API_URL"):
96
97
  cfg["api_base_url"] = env_url
98
+ if env_sandbox := os.environ.get("LEGION_SANDBOX"):
99
+ cfg["sandbox"] = env_sandbox.strip().lower() in ("1", "true", "yes")
97
100
  return cfg
98
101
 
99
102
 
@@ -127,3 +130,8 @@ def get_api_key() -> str:
127
130
 
128
131
  def get_base_url() -> str:
129
132
  return str(load_config().get("api_base_url", _DEFAULTS["api_base_url"]))
133
+
134
+
135
+ def get_sandbox() -> bool:
136
+ """Sandbox mode: the --sandbox flag, LEGION_SANDBOX, or `config set sandbox true`."""
137
+ return load_config().get("sandbox") is True
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from typing import Annotated
6
7
 
7
8
  import typer
@@ -36,9 +37,19 @@ app.add_typer(credits.app, name="credits")
36
37
  def main_callback(
37
38
  verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable debug logging.")] = False,
38
39
  quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress all log output.")] = False,
40
+ sandbox: Annotated[
41
+ bool,
42
+ typer.Option(
43
+ "--sandbox",
44
+ help="Return sample records from enrich, search and discover, using no credits.",
45
+ ),
46
+ ] = False,
39
47
  ) -> None:
40
48
  """Data Legion CLI."""
41
49
  setup_logging(verbose=verbose, quiet=quiet)
50
+ if sandbox:
51
+ # Read by config.load_config, so every client this run creates is in sandbox mode.
52
+ os.environ["LEGION_SANDBOX"] = "1"
42
53
 
43
54
 
44
55
  # ── Top-level commands ───────────────────────────────────────────────
@@ -210,3 +210,47 @@ class TestConvenienceMethods:
210
210
  http = await self._setup_mock(mock_client, monkeypatch)
211
211
  await mock_client.credits_usage(start_date="2026-01-01", end_date="2026-03-01")
212
212
  assert http.get.call_args[0][0] == "/credits/usage"
213
+
214
+
215
+ @pytest.mark.asyncio
216
+ class TestSandbox:
217
+ """Sandbox mode sends only the endpoints the API mirrors to /sandbox/."""
218
+
219
+ async def _paths(self, monkeypatch, sandbox_env: str | None, endpoints: list[str]) -> list[str]:
220
+ if sandbox_env is None:
221
+ monkeypatch.delenv("LEGION_SANDBOX", raising=False)
222
+ else:
223
+ monkeypatch.setenv("LEGION_SANDBOX", sandbox_env)
224
+ client = LegionClient(base_url="https://x.local", api_key="legion_k")
225
+ mock_post = AsyncMock(return_value=make_response(200, {"matches": []}))
226
+ client._client = AsyncMock()
227
+ client._client.is_closed = False
228
+ client._client.post = mock_post
229
+ for endpoint in endpoints:
230
+ await client.request("POST", endpoint, payload={})
231
+ return [call.args[0] for call in mock_post.call_args_list]
232
+
233
+ async def test_sandbox_routes_only_mirrored_paths(self, monkeypatch):
234
+ paths = await self._paths(
235
+ monkeypatch, "1", ["/person/enrich", "/company/search", "/utility/clean"]
236
+ )
237
+ assert paths == ["/sandbox/person/enrich", "/sandbox/company/search", "/utility/clean"]
238
+
239
+ async def test_off_by_default(self, monkeypatch):
240
+ assert await self._paths(monkeypatch, None, ["/person/enrich"]) == ["/person/enrich"]
241
+
242
+ async def test_env_false_is_off(self, monkeypatch):
243
+ assert await self._paths(monkeypatch, "false", ["/person/enrich"]) == ["/person/enrich"]
244
+
245
+
246
+ def test_sandbox_flag_turns_it_on(monkeypatch):
247
+ from typer.testing import CliRunner
248
+
249
+ from datalegion_cli.main import app
250
+
251
+ monkeypatch.delenv("LEGION_SANDBOX", raising=False)
252
+ CliRunner().invoke(app, ["--sandbox", "version"])
253
+ import os
254
+
255
+ assert os.environ.get("LEGION_SANDBOX") == "1"
256
+ monkeypatch.delenv("LEGION_SANDBOX", raising=False)