datalegion-cli 1.7.1__tar.gz → 1.9.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 (31) hide show
  1. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/PKG-INFO +35 -9
  2. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/README.md +34 -8
  3. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/pyproject.toml +9 -2
  4. datalegion_cli-1.9.0/src/datalegion_cli/commands/__init__.py +26 -0
  5. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/company.py +20 -3
  6. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/person.py +25 -3
  7. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_commands.py +60 -0
  8. datalegion_cli-1.7.1/src/datalegion_cli/commands/__init__.py +0 -1
  9. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/.gitignore +0 -0
  10. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/LICENSE +0 -0
  11. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/__init__.py +0 -0
  12. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/async_typer.py +0 -0
  13. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/client.py +0 -0
  14. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/credits.py +0 -0
  15. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/utility.py +0 -0
  16. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/config.py +0 -0
  17. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/logging.py +0 -0
  18. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/login.py +0 -0
  19. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/main.py +0 -0
  20. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/output.py +0 -0
  21. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/update.py +0 -0
  22. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/__init__.py +0 -0
  23. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/conftest.py +0 -0
  24. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_client.py +0 -0
  25. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_config.py +0 -0
  26. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_live.py +0 -0
  27. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_logging.py +0 -0
  28. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_login.py +0 -0
  29. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_output.py +0 -0
  30. {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_package_contents.py +0 -0
  31. {datalegion_cli-1.7.1 → datalegion_cli-1.9.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.7.1
3
+ Version: 1.9.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
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  The official command-line interface for the [Data Legion](https://www.datalegion.ai) API. Built for both humans and AI agents.
39
39
 
40
- Look up people and companies by email, phone, name, domain, LinkedIn, or ticker symbol. Search using SQL syntax. Clean, hash, and validate data fields before or after enrichment. Track credit usage and manage your configuration — all from the terminal.
40
+ Look up people and companies by email, phone, name, domain, LinkedIn, or ticker symbol. Search using SQL syntax. Clean, hash, and validate data fields before or after enrichment. Track credit usage and manage your configuration, all from the terminal.
41
41
 
42
42
  ```bash
43
43
  pip install datalegion-cli
@@ -47,12 +47,12 @@ datalegion-cli person enrich --email john@example.com
47
47
 
48
48
  ## Features
49
49
 
50
- - **Fully async** — async from entry point to HTTP client, powered by `asyncio` and `httpx`
51
- - **Agent-friendly** — all output is JSON to stdout, logs go to stderr, supports `--stdin` for piping
50
+ - **Fully async**: async from entry point to HTTP client, powered by `asyncio` and `httpx`
51
+ - **Agent-friendly**: all output is JSON to stdout, logs go to stderr, supports `--stdin` for piping
52
52
  - **Search warnings**: when `search` or `discover` flags one of your filters (for example an industry name no stored value matches), the API's explanation is printed on stderr as `search warning: ...`; stdout stays JSON
53
- - **Encrypted config** — API keys are stored encrypted at rest (Fernet + PBKDF2, machine-tied)
54
- - **Auto-update** — checks PyPI for a newer release and self-updates via Homebrew, GitHub release, git pull, or pip/uv
55
- - **Multiple install methods** — Homebrew, GitHub release, pip/uv (with private index support), or from source
53
+ - **Encrypted config**: API keys are stored encrypted at rest (Fernet + PBKDF2, machine-tied)
54
+ - **Auto-update**: checks PyPI for a newer release and self-updates via Homebrew, GitHub release, git pull, or pip/uv
55
+ - **Multiple install methods**: Homebrew, GitHub release, pip/uv (with private index support), or from source
56
56
 
57
57
  ## Installation
58
58
 
@@ -165,6 +165,9 @@ datalegion-cli person enrich --name "Jane Smith" --company "Acme Corp" --city "N
165
165
  # By LinkedIn
166
166
  datalegion-cli person enrich --social-url "https://linkedin.com/in/johndoe"
167
167
 
168
+ # By NPI (10-digit National Provider Identifier)
169
+ datalegion-cli person enrich --npi 1234567893
170
+
168
171
  # Minimum confidence
169
172
  datalegion-cli person enrich --email john@example.com --min-confidence high
170
173
 
@@ -179,6 +182,13 @@ datalegion-cli person enrich --email john@example.com --fields "full_name,emails
179
182
  datalegion-cli person search "SELECT * FROM people WHERE company_name ILIKE '%google%' AND state = 'CA'" --limit 5
180
183
  ```
181
184
 
185
+ `query` can also be an Elasticsearch-style query object as JSON (`bool`, `term`, `terms`, `match`, `match_phrase`, `range`, `exists`, `prefix`, `wildcard`, `nested`), on the response's own field names. For deep paging, pass each page's `next_cursor` back with `--cursor`; the last page has none. Company search takes both too.
186
+
187
+ ```bash
188
+ datalegion-cli person search '{"bool": {"filter": [{"term": {"company_name": "google"}}, {"match": {"job_title": "engineer"}}]}}' --limit 100
189
+ datalegion-cli person search "SELECT * FROM people WHERE state = 'ohio'" --limit 100 --cursor "<next_cursor from the last page>"
190
+ ```
191
+
182
192
  ### Person Discovery (Beta)
183
193
 
184
194
  ```bash
@@ -186,6 +196,22 @@ datalegion-cli person search "SELECT * FROM people WHERE company_name ILIKE '%go
186
196
  datalegion-cli person discover "software engineers in San Francisco who know Rust" --limit 10
187
197
  ```
188
198
 
199
+ For the next page, pass the last page's `next_cursor` with `--cursor` and the same query: it reruns the same search. Company discover takes it too.
200
+
201
+ ```bash
202
+ query="software engineers in San Francisco who know Rust"
203
+ cursor=""
204
+ while :; do
205
+ # A failed page (a 429, 402 or 403) prints its error and stops the loop
206
+ page=$(datalegion-cli person discover "$query" --limit 100 ${cursor:+--cursor} ${cursor:+"$cursor"}) || { printf '%s\n' "$page" >&2; break; }
207
+ printf '%s\n' "$page" | jq -r '.matches[].person.legion_id'
208
+ cursor=$(printf '%s\n' "$page" | jq -r '.next_cursor // empty')
209
+ [ -z "$cursor" ] && break
210
+ sleep 6 # Discover answers 10 requests a minute
211
+ done
212
+ [ -n "$cursor" ] && echo "stopped early; resume with --cursor $cursor" >&2
213
+ ```
214
+
189
215
  ### Company Enrichment
190
216
 
191
217
  ```bash
@@ -258,7 +284,7 @@ datalegion-cli version
258
284
  The CLI is designed to be called by AI agents and scripts. Key patterns:
259
285
 
260
286
  ```bash
261
- # All output is JSON to stdout — parse it directly
287
+ # All output is JSON to stdout; parse it directly
262
288
  datalegion-cli person enrich --email john@example.com | jq '.matches[0].person.full_name'
263
289
 
264
290
  # Pipe JSON payloads via stdin
@@ -274,7 +300,7 @@ datalegion-cli person enrich --email bad 2>/dev/null
274
300
 
275
301
  ## Sandbox
276
302
 
277
- 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.
303
+ Try commands without spending credits: `--sandbox` (or `LEGION_SANDBOX=1`, or `datalegion-cli config set sandbox true`) makes 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.
278
304
 
279
305
  ```bash
280
306
  datalegion-cli --sandbox person enrich --email anyone@example.com
@@ -2,7 +2,7 @@
2
2
 
3
3
  The official command-line interface for the [Data Legion](https://www.datalegion.ai) API. Built for both humans and AI agents.
4
4
 
5
- Look up people and companies by email, phone, name, domain, LinkedIn, or ticker symbol. Search using SQL syntax. Clean, hash, and validate data fields before or after enrichment. Track credit usage and manage your configuration — all from the terminal.
5
+ Look up people and companies by email, phone, name, domain, LinkedIn, or ticker symbol. Search using SQL syntax. Clean, hash, and validate data fields before or after enrichment. Track credit usage and manage your configuration, all from the terminal.
6
6
 
7
7
  ```bash
8
8
  pip install datalegion-cli
@@ -12,12 +12,12 @@ datalegion-cli person enrich --email john@example.com
12
12
 
13
13
  ## Features
14
14
 
15
- - **Fully async** — async from entry point to HTTP client, powered by `asyncio` and `httpx`
16
- - **Agent-friendly** — all output is JSON to stdout, logs go to stderr, supports `--stdin` for piping
15
+ - **Fully async**: async from entry point to HTTP client, powered by `asyncio` and `httpx`
16
+ - **Agent-friendly**: all output is JSON to stdout, logs go to stderr, supports `--stdin` for piping
17
17
  - **Search warnings**: when `search` or `discover` flags one of your filters (for example an industry name no stored value matches), the API's explanation is printed on stderr as `search warning: ...`; stdout stays JSON
18
- - **Encrypted config** — API keys are stored encrypted at rest (Fernet + PBKDF2, machine-tied)
19
- - **Auto-update** — checks PyPI for a newer release and self-updates via Homebrew, GitHub release, git pull, or pip/uv
20
- - **Multiple install methods** — Homebrew, GitHub release, pip/uv (with private index support), or from source
18
+ - **Encrypted config**: API keys are stored encrypted at rest (Fernet + PBKDF2, machine-tied)
19
+ - **Auto-update**: checks PyPI for a newer release and self-updates via Homebrew, GitHub release, git pull, or pip/uv
20
+ - **Multiple install methods**: Homebrew, GitHub release, pip/uv (with private index support), or from source
21
21
 
22
22
  ## Installation
23
23
 
@@ -130,6 +130,9 @@ datalegion-cli person enrich --name "Jane Smith" --company "Acme Corp" --city "N
130
130
  # By LinkedIn
131
131
  datalegion-cli person enrich --social-url "https://linkedin.com/in/johndoe"
132
132
 
133
+ # By NPI (10-digit National Provider Identifier)
134
+ datalegion-cli person enrich --npi 1234567893
135
+
133
136
  # Minimum confidence
134
137
  datalegion-cli person enrich --email john@example.com --min-confidence high
135
138
 
@@ -144,6 +147,13 @@ datalegion-cli person enrich --email john@example.com --fields "full_name,emails
144
147
  datalegion-cli person search "SELECT * FROM people WHERE company_name ILIKE '%google%' AND state = 'CA'" --limit 5
145
148
  ```
146
149
 
150
+ `query` can also be an Elasticsearch-style query object as JSON (`bool`, `term`, `terms`, `match`, `match_phrase`, `range`, `exists`, `prefix`, `wildcard`, `nested`), on the response's own field names. For deep paging, pass each page's `next_cursor` back with `--cursor`; the last page has none. Company search takes both too.
151
+
152
+ ```bash
153
+ datalegion-cli person search '{"bool": {"filter": [{"term": {"company_name": "google"}}, {"match": {"job_title": "engineer"}}]}}' --limit 100
154
+ datalegion-cli person search "SELECT * FROM people WHERE state = 'ohio'" --limit 100 --cursor "<next_cursor from the last page>"
155
+ ```
156
+
147
157
  ### Person Discovery (Beta)
148
158
 
149
159
  ```bash
@@ -151,6 +161,22 @@ datalegion-cli person search "SELECT * FROM people WHERE company_name ILIKE '%go
151
161
  datalegion-cli person discover "software engineers in San Francisco who know Rust" --limit 10
152
162
  ```
153
163
 
164
+ For the next page, pass the last page's `next_cursor` with `--cursor` and the same query: it reruns the same search. Company discover takes it too.
165
+
166
+ ```bash
167
+ query="software engineers in San Francisco who know Rust"
168
+ cursor=""
169
+ while :; do
170
+ # A failed page (a 429, 402 or 403) prints its error and stops the loop
171
+ page=$(datalegion-cli person discover "$query" --limit 100 ${cursor:+--cursor} ${cursor:+"$cursor"}) || { printf '%s\n' "$page" >&2; break; }
172
+ printf '%s\n' "$page" | jq -r '.matches[].person.legion_id'
173
+ cursor=$(printf '%s\n' "$page" | jq -r '.next_cursor // empty')
174
+ [ -z "$cursor" ] && break
175
+ sleep 6 # Discover answers 10 requests a minute
176
+ done
177
+ [ -n "$cursor" ] && echo "stopped early; resume with --cursor $cursor" >&2
178
+ ```
179
+
154
180
  ### Company Enrichment
155
181
 
156
182
  ```bash
@@ -223,7 +249,7 @@ datalegion-cli version
223
249
  The CLI is designed to be called by AI agents and scripts. Key patterns:
224
250
 
225
251
  ```bash
226
- # All output is JSON to stdout — parse it directly
252
+ # All output is JSON to stdout; parse it directly
227
253
  datalegion-cli person enrich --email john@example.com | jq '.matches[0].person.full_name'
228
254
 
229
255
  # Pipe JSON payloads via stdin
@@ -239,7 +265,7 @@ datalegion-cli person enrich --email bad 2>/dev/null
239
265
 
240
266
  ## Sandbox
241
267
 
242
- 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.
268
+ Try commands without spending credits: `--sandbox` (or `LEGION_SANDBOX=1`, or `datalegion-cli config set sandbox true`) makes 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.
243
269
 
244
270
  ```bash
245
271
  datalegion-cli --sandbox person enrich --email anyone@example.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datalegion-cli"
3
- version = "1.7.1"
3
+ version = "1.9.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"
@@ -103,5 +103,12 @@ ignore = [
103
103
  known-first-party = ["datalegion_cli"]
104
104
 
105
105
  [tool.pytest.ini_options]
106
+ asyncio_default_fixture_loop_scope = "function"
106
107
  asyncio_mode = "auto"
107
- testpaths = ["tests"]
108
+ testpaths = ["tests"]
109
+
110
+ [tool.uv]
111
+ # 0.11.8 writes the lock's sentinel form for a relative exclude-newer (below).
112
+ required-version = ">=0.11.8"
113
+ # The fleet's one-day cooldown: no release under 24 h old is resolved, by any uv command here.
114
+ exclude-newer = "1 day"
@@ -0,0 +1,26 @@
1
+ """CLI command modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+
10
+ CursorOpt = Annotated[
11
+ str | None,
12
+ typer.Option(
13
+ "--cursor",
14
+ help="The previous page's next_cursor, with the same query, for the next page (search: in place of --offset).",
15
+ ),
16
+ ]
17
+
18
+
19
+ def search_query(text: str) -> str | dict[str, Any]:
20
+ """SQL as given, or a query object when the argument is a JSON object."""
21
+ if not text.lstrip().startswith("{"):
22
+ return text
23
+ try:
24
+ return json.loads(text)
25
+ except json.JSONDecodeError as e:
26
+ raise typer.BadParameter(f"The query looks like JSON but doesn't parse: {e.msg}.") from e
@@ -10,6 +10,7 @@ import typer
10
10
 
11
11
  from datalegion_cli.async_typer import async_command
12
12
  from datalegion_cli.client import APIError, LegionClient
13
+ from datalegion_cli.commands import CursorOpt, search_query
13
14
  from datalegion_cli.output import emit_credits, emit_error, emit_json, emit_search_warning
14
15
 
15
16
  app = typer.Typer(name="company", help="Company enrichment, search, and discovery.")
@@ -102,8 +103,14 @@ async def enrich(
102
103
  @app.command()
103
104
  @async_command
104
105
  async def search(
105
- query: Annotated[str, typer.Argument(help="SQL SELECT query against the companies table.")],
106
+ query: Annotated[
107
+ str,
108
+ typer.Argument(
109
+ help="SQL SELECT query against the companies table, or a query object as JSON."
110
+ ),
111
+ ],
106
112
  limit: LimitOpt = 1,
113
+ cursor: CursorOpt = None,
107
114
  titlecase: TitleOpt = False,
108
115
  include_fields: FieldsInclude = None,
109
116
  exclude_fields: FieldsExclude = None,
@@ -114,12 +121,15 @@ async def search(
114
121
  datalegion-cli company search "SELECT * FROM companies WHERE industry ILIKE '%software%' AND legion_employee_count > 100"
115
122
  datalegion-cli company search "SELECT * FROM companies WHERE name ILIKE '%tech%'" --limit 20
116
123
  datalegion-cli company search "SELECT * FROM companies WHERE domain = 'stripe.com'" --fields "name,domain,industry"
124
+ datalegion-cli company search '{"term": {"domain": "stripe.com"}}'
125
+ datalegion-cli company search "SELECT * FROM companies WHERE industry = 'banking'" --limit 100 --cursor <next_cursor>
117
126
  """
118
127
  client = LegionClient()
119
128
  try:
120
129
  result = await client.company_search(
121
- query=query,
130
+ query=search_query(query),
122
131
  limit=limit,
132
+ cursor=cursor,
123
133
  titlecase=titlecase,
124
134
  include_fields=include_fields,
125
135
  exclude_fields=exclude_fields,
@@ -139,16 +149,23 @@ async def search(
139
149
  async def discover(
140
150
  query: Annotated[str, typer.Argument(help="Natural language company search query.")],
141
151
  limit: LimitOpt = 1,
152
+ cursor: CursorOpt = None,
142
153
  titlecase: TitleOpt = False,
143
154
  include_fields: FieldsInclude = None,
144
155
  exclude_fields: FieldsExclude = None,
145
156
  ) -> None:
146
- """Search for companies using natural language (Beta)."""
157
+ """Search for companies using natural language (Beta).
158
+
159
+ For the next page, pass the previous page's next_cursor as --cursor with the same query: it reruns the same search.
160
+
161
+ datalegion-cli company discover "banks in Ohio" --limit 100 --cursor <next_cursor>
162
+ """
147
163
  client = LegionClient()
148
164
  try:
149
165
  result = await client.company_discover(
150
166
  query=query,
151
167
  limit=limit,
168
+ cursor=cursor,
152
169
  titlecase=titlecase,
153
170
  include_fields=include_fields,
154
171
  exclude_fields=exclude_fields,
@@ -10,6 +10,7 @@ import typer
10
10
 
11
11
  from datalegion_cli.async_typer import async_command
12
12
  from datalegion_cli.client import APIError, LegionClient
13
+ from datalegion_cli.commands import CursorOpt, search_query
13
14
  from datalegion_cli.output import emit_credits, emit_error, emit_json, emit_search_warning
14
15
 
15
16
  app = typer.Typer(name="person", help="Person enrichment, search, and discovery.")
@@ -52,6 +53,9 @@ async def enrich(
52
53
  email_hash: Annotated[
53
54
  str | None, typer.Option("--email-hash", help="SHA-256/SHA-1/MD5 email hash.")
54
55
  ] = None,
56
+ npi: Annotated[
57
+ str | None, typer.Option("--npi", help="10-digit National Provider Identifier.")
58
+ ] = None,
55
59
  full_name: Annotated[str | None, typer.Option("--name", "-n", help="Full name.")] = None,
56
60
  first_name: Annotated[str | None, typer.Option("--first-name", help="First name.")] = None,
57
61
  last_name: Annotated[str | None, typer.Option("--last-name", help="Last name.")] = None,
@@ -82,6 +86,7 @@ async def enrich(
82
86
  Examples:
83
87
  datalegion-cli person enrich --email john@example.com
84
88
  datalegion-cli person enrich --name "Jane Doe" --company "Acme Corp"
89
+ datalegion-cli person enrich --npi 1234567893
85
90
  datalegion-cli person enrich --phone "+14155551234" --fields "full_name,work_email,job_title"
86
91
  echo '{"email":"jane@example.com"}' | datalegion-cli person enrich --stdin
87
92
  """
@@ -95,6 +100,7 @@ async def enrich(
95
100
  social_url=social_url,
96
101
  legion_id=legion_id,
97
102
  email_hash=email_hash,
103
+ npi=npi,
98
104
  full_name=full_name,
99
105
  first_name=first_name,
100
106
  last_name=last_name,
@@ -128,8 +134,14 @@ async def enrich(
128
134
  @app.command()
129
135
  @async_command
130
136
  async def search(
131
- query: Annotated[str, typer.Argument(help="SQL SELECT query against the people table.")],
137
+ query: Annotated[
138
+ str,
139
+ typer.Argument(
140
+ help="SQL SELECT query against the people table, or a query object as JSON."
141
+ ),
142
+ ],
132
143
  limit: LimitOpt = 1,
144
+ cursor: CursorOpt = None,
133
145
  titlecase: TitleOpt = False,
134
146
  include_fields: FieldsInclude = None,
135
147
  exclude_fields: FieldsExclude = None,
@@ -139,12 +151,15 @@ async def search(
139
151
  Examples:
140
152
  datalegion-cli person search "SELECT * FROM people WHERE job_title = 'ceo' AND company_name ILIKE '%acme%'"
141
153
  datalegion-cli person search "SELECT * FROM people WHERE city = 'san francisco'" --limit 20
154
+ datalegion-cli person search '{"term": {"company_name": "acme"}}' --limit 100
155
+ datalegion-cli person search "SELECT * FROM people WHERE state = 'ohio'" --limit 100 --cursor <next_cursor>
142
156
  """
143
157
  client = LegionClient()
144
158
  try:
145
159
  result = await client.person_search(
146
- query=query,
160
+ query=search_query(query),
147
161
  limit=limit,
162
+ cursor=cursor,
148
163
  titlecase=titlecase,
149
164
  include_fields=include_fields,
150
165
  exclude_fields=exclude_fields,
@@ -164,16 +179,23 @@ async def search(
164
179
  async def discover(
165
180
  query: Annotated[str, typer.Argument(help="Natural language search query.")],
166
181
  limit: LimitOpt = 1,
182
+ cursor: CursorOpt = None,
167
183
  titlecase: TitleOpt = False,
168
184
  include_fields: FieldsInclude = None,
169
185
  exclude_fields: FieldsExclude = None,
170
186
  ) -> None:
171
- """Search for people using natural language (Beta)."""
187
+ """Search for people using natural language (Beta).
188
+
189
+ For the next page, pass the previous page's next_cursor as --cursor with the same query: it reruns the same search.
190
+
191
+ datalegion-cli person discover "nurses in Ohio" --limit 100 --cursor <next_cursor>
192
+ """
172
193
  client = LegionClient()
173
194
  try:
174
195
  result = await client.person_discover(
175
196
  query=query,
176
197
  limit=limit,
198
+ cursor=cursor,
177
199
  titlecase=titlecase,
178
200
  include_fields=include_fields,
179
201
  exclude_fields=exclude_fields,
@@ -135,6 +135,20 @@ class TestPersonEnrich:
135
135
  )
136
136
  assert result.exit_code == 0
137
137
 
138
+ def test_enrich_by_npi_sends_it(self, monkeypatch):
139
+ _mock_config(monkeypatch)
140
+ sent = {}
141
+
142
+ async def fake_request(self, method, endpoint, *, payload=None, params=None):
143
+ sent.update(endpoint=endpoint, payload=payload)
144
+ return {"matches": [{"full_name": "Jane Smith"}]}
145
+
146
+ with patch("datalegion_cli.client.LegionClient.request", fake_request):
147
+ result = runner.invoke(app, ["person", "enrich", "--npi", "1234567893"])
148
+ assert result.exit_code == 0, result.output
149
+ assert sent["endpoint"].endswith("/person/enrich")
150
+ assert sent["payload"]["npi"] == "1234567893"
151
+
138
152
  def test_enrich_stdin(self, monkeypatch):
139
153
  _mock_config(monkeypatch)
140
154
  response = {"matches": [{"full_name": "Bob"}], "total": 1}
@@ -168,6 +182,52 @@ class TestPersonSearch:
168
182
  assert result.exit_code == 0
169
183
 
170
184
 
185
+ class TestSearchQueryObjectAndCursor:
186
+ @pytest.mark.parametrize("entity", ["person", "company"])
187
+ def test_a_json_query_goes_as_an_object_with_the_cursor(self, monkeypatch, entity):
188
+ _mock_config(monkeypatch)
189
+ sent = []
190
+
191
+ async def fake_request(self, method, endpoint, *, payload=None, params=None):
192
+ sent.append(payload)
193
+ return {"matches": [], "total": 0, "next_cursor": "abc"}
194
+
195
+ monkeypatch.setattr("datalegion_cli.client.LegionClient.request", fake_request)
196
+ result = runner.invoke(
197
+ app, [entity, "search", '{"exists": {"field": "legion_id"}}', "--cursor", "abc"]
198
+ )
199
+ assert result.exit_code == 0, result.output
200
+ assert sent[0]["query"] == {"exists": {"field": "legion_id"}}
201
+ assert sent[0]["cursor"] == "abc"
202
+ assert json.loads(result.stdout)["next_cursor"] == "abc"
203
+
204
+ @pytest.mark.parametrize("entity", ["person", "company"])
205
+ def test_discover_sends_the_cursor_and_prints_next_cursor(self, monkeypatch, entity):
206
+ _mock_config(monkeypatch)
207
+ sent = []
208
+
209
+ async def fake_request(self, method, endpoint, *, payload=None, params=None):
210
+ sent.append((endpoint, payload))
211
+ return {"matches": [], "total": 0, "next_cursor": "def", "generated_query": "SELECT 1"}
212
+
213
+ monkeypatch.setattr("datalegion_cli.client.LegionClient.request", fake_request)
214
+ result = runner.invoke(app, [entity, "discover", "nurses in Ohio", "--cursor", "abc"])
215
+ assert result.exit_code == 0, result.output
216
+ assert sent[0][0] == f"/{entity}/discover"
217
+ assert sent[0][1]["query"] == "nurses in Ohio" and sent[0][1]["cursor"] == "abc"
218
+ assert json.loads(result.stdout)["next_cursor"] == "def"
219
+
220
+ def test_a_query_that_looks_like_json_but_does_not_parse_is_refused(self, monkeypatch):
221
+ _mock_config(monkeypatch)
222
+ result = runner.invoke(app, ["person", "search", "{not json"])
223
+ assert result.exit_code == 2
224
+
225
+ def test_sql_stays_a_string(self):
226
+ from datalegion_cli.commands import search_query
227
+
228
+ assert search_query("SELECT * FROM people") == "SELECT * FROM people"
229
+
230
+
171
231
  class TestPersonDiscover:
172
232
  def test_discover(self, monkeypatch):
173
233
  _mock_config(monkeypatch)
@@ -1 +0,0 @@
1
- """CLI command modules."""
File without changes