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.
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/PKG-INFO +35 -9
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/README.md +34 -8
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/pyproject.toml +9 -2
- datalegion_cli-1.9.0/src/datalegion_cli/commands/__init__.py +26 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/company.py +20 -3
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/person.py +25 -3
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_commands.py +60 -0
- datalegion_cli-1.7.1/src/datalegion_cli/commands/__init__.py +0 -1
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/.gitignore +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/LICENSE +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/__init__.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/async_typer.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/client.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/credits.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/commands/utility.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/config.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/logging.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/login.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/main.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/output.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/src/datalegion_cli/update.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/__init__.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/conftest.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_client.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_config.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_live.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_logging.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_login.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_output.py +0 -0
- {datalegion_cli-1.7.1 → datalegion_cli-1.9.0}/tests/test_package_contents.py +0 -0
- {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.
|
|
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
|
|
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
|
|
51
|
-
- **Agent-friendly
|
|
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
|
|
54
|
-
- **Auto-update
|
|
55
|
-
- **Multiple install methods
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16
|
-
- **Agent-friendly
|
|
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
|
|
19
|
-
- **Auto-update
|
|
20
|
-
- **Multiple install methods
|
|
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
|
|
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
|
|
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.
|
|
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[
|
|
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[
|
|
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
|
|
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
|