datalegion-cli 1.1.0__tar.gz → 1.2.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 (30) hide show
  1. datalegion_cli-1.2.0/.github/dependabot.yml +42 -0
  2. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/.github/workflows/ci.yml +6 -6
  3. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/.github/workflows/release.yml +8 -8
  4. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/PKG-INFO +22 -4
  5. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/README.md +21 -3
  6. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/pyproject.toml +1 -1
  7. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/client.py +7 -4
  8. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/commands/company.py +29 -27
  9. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/commands/person.py +29 -27
  10. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/commands/utility.py +68 -2
  11. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/test_commands.py +85 -21
  12. datalegion_cli-1.2.0/uv.lock +645 -0
  13. datalegion_cli-1.1.0/uv.lock +0 -627
  14. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/.gitignore +0 -0
  15. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/__init__.py +0 -0
  16. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/async_typer.py +0 -0
  17. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/commands/__init__.py +0 -0
  18. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/commands/credits.py +0 -0
  19. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/config.py +0 -0
  20. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/logging.py +0 -0
  21. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/main.py +0 -0
  22. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/output.py +0 -0
  23. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/src/datalegion_cli/update.py +0 -0
  24. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/__init__.py +0 -0
  25. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/conftest.py +0 -0
  26. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/test_client.py +0 -0
  27. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/test_config.py +0 -0
  28. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/test_logging.py +0 -0
  29. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/test_output.py +0 -0
  30. {datalegion_cli-1.1.0 → datalegion_cli-1.2.0}/tests/test_update.py +0 -0
@@ -0,0 +1,42 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "uv"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ day: "sunday"
8
+ time: "09:00"
9
+ timezone: "America/Los_Angeles"
10
+ cooldown:
11
+ default-days: 1
12
+ labels:
13
+ - "dependencies"
14
+ - "python"
15
+ commit-message:
16
+ prefix: "chore(deps)"
17
+ groups:
18
+ minor-patch:
19
+ patterns:
20
+ - "*"
21
+ update-types:
22
+ - "minor"
23
+ - "patch"
24
+
25
+ - package-ecosystem: "github-actions"
26
+ directory: "/"
27
+ schedule:
28
+ interval: "weekly"
29
+ day: "sunday"
30
+ time: "09:00"
31
+ timezone: "America/Los_Angeles"
32
+ cooldown:
33
+ default-days: 1
34
+ labels:
35
+ - "dependencies"
36
+ - "github-actions"
37
+ commit-message:
38
+ prefix: "chore(deps)"
39
+ groups:
40
+ actions:
41
+ patterns:
42
+ - "*"
@@ -8,11 +8,11 @@ jobs:
8
8
  lint:
9
9
  runs-on: blacksmith-2vcpu-ubuntu-2404
10
10
  steps:
11
- - uses: actions/checkout@v6
12
- - uses: astral-sh/setup-uv@v7
11
+ - uses: actions/checkout@v7
12
+ - uses: astral-sh/setup-uv@v9.0.0
13
13
  with:
14
14
  enable-cache: true
15
- - uses: actions/setup-python@v6
15
+ - uses: actions/setup-python@v7
16
16
  with:
17
17
  python-version: "3.11"
18
18
  - run: uv sync --group dev
@@ -22,11 +22,11 @@ jobs:
22
22
  test:
23
23
  runs-on: blacksmith-2vcpu-ubuntu-2404
24
24
  steps:
25
- - uses: actions/checkout@v6
26
- - uses: astral-sh/setup-uv@v7
25
+ - uses: actions/checkout@v7
26
+ - uses: astral-sh/setup-uv@v9.0.0
27
27
  with:
28
28
  enable-cache: true
29
- - uses: actions/setup-python@v6
29
+ - uses: actions/setup-python@v7
30
30
  with:
31
31
  python-version: "3.11"
32
32
  - run: uv sync --group dev
@@ -18,10 +18,10 @@ jobs:
18
18
  contents: read
19
19
  steps:
20
20
  - name: Checkout
21
- uses: actions/checkout@v6
21
+ uses: actions/checkout@v7
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@v7
24
+ uses: astral-sh/setup-uv@v9.0.0
25
25
  with:
26
26
  enable-cache: true
27
27
  cache-dependency-glob: "uv.lock"
@@ -57,10 +57,10 @@ jobs:
57
57
  python-version: ["3.11", "3.12", "3.13"]
58
58
  steps:
59
59
  - name: Checkout
60
- uses: actions/checkout@v6
60
+ uses: actions/checkout@v7
61
61
 
62
62
  - name: Install uv
63
- uses: astral-sh/setup-uv@v7
63
+ uses: astral-sh/setup-uv@v9.0.0
64
64
  with:
65
65
  enable-cache: true
66
66
  cache-dependency-glob: "uv.lock"
@@ -88,10 +88,10 @@ jobs:
88
88
  id-token: write
89
89
  steps:
90
90
  - name: Checkout
91
- uses: actions/checkout@v6
91
+ uses: actions/checkout@v7
92
92
 
93
93
  - name: Install uv
94
- uses: astral-sh/setup-uv@v7
94
+ uses: astral-sh/setup-uv@v9.0.0
95
95
 
96
96
  - name: Set up Python
97
97
  run: uv python install 3.11
@@ -200,7 +200,7 @@ jobs:
200
200
  contents: write
201
201
  steps:
202
202
  - name: Checkout
203
- uses: actions/checkout@v6
203
+ uses: actions/checkout@v7
204
204
  with:
205
205
  ref: main
206
206
  token: ${{ secrets.GH_PAT }}
@@ -216,7 +216,7 @@ jobs:
216
216
  git push
217
217
 
218
218
  - name: Create Release
219
- uses: actions/github-script@v8
219
+ uses: actions/github-script@v9
220
220
  with:
221
221
  script: |
222
222
  const tag = context.ref.replace('refs/tags/', '');
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalegion-cli
3
- Version: 1.1.0
3
+ Version: 1.2.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
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  The official command-line interface for the [Data Legion](https://www.datalegion.ai) API. Built for both humans and AI agents.
37
37
 
38
- Look up people and companies by email, phone, name, domain, LinkedIn, or ticker symbol. Search across 1.5B+ contacts and 200M+ companies using SQL syntax. Clean, hash, and validate data fields before or after enrichment. Track credit usage and manage your configuration — all from the terminal.
38
+ 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.
39
39
 
40
40
  ```bash
41
41
  pip install datalegion-cli
@@ -159,13 +159,20 @@ datalegion-cli person enrich --email john@example.com --multiple --min-confidenc
159
159
  datalegion-cli person enrich --email john@example.com --fields "full_name,emails,phones"
160
160
  ```
161
161
 
162
- ### Person search
162
+ ### Person search (Beta)
163
163
 
164
164
  ```bash
165
165
  # SQL-based search
166
166
  datalegion-cli person search "SELECT * FROM people WHERE company_name ILIKE '%google%' AND state = 'CA'" --limit 5
167
167
  ```
168
168
 
169
+ ### Person discovery (Beta)
170
+
171
+ ```bash
172
+ # Natural-language search
173
+ datalegion-cli person discover "software engineers in San Francisco who know Rust" --limit 10
174
+ ```
175
+
169
176
  ### Company enrichment
170
177
 
171
178
  ```bash
@@ -179,13 +186,20 @@ datalegion-cli company enrich --ticker AAPL
179
186
  datalegion-cli company enrich --name "Stripe" --industry "Financial Services"
180
187
  ```
181
188
 
182
- ### Company search
189
+ ### Company search (Beta)
183
190
 
184
191
  ```bash
185
192
  # SQL-based search
186
193
  datalegion-cli company search "SELECT * FROM companies WHERE industry ILIKE '%software%' AND legion_employee_count > 100"
187
194
  ```
188
195
 
196
+ ### Company discovery (Beta)
197
+
198
+ ```bash
199
+ # Natural-language search
200
+ datalegion-cli company discover "fintech startups in New York with under 50 employees"
201
+ ```
202
+
189
203
  ### Utility commands
190
204
 
191
205
  ```bash
@@ -197,6 +211,10 @@ datalegion-cli utility hash john@example.com
197
211
 
198
212
  # Validate fields before enrichment
199
213
  datalegion-cli utility validate --email "test@example.com" --phone "+15551234567"
214
+
215
+ # Report a data issue on a record (no credit cost; include --correct or --comment)
216
+ datalegion-cli utility report person --legion-id abc123 --issue-type outdated \
217
+ --issue-level field --field job_title --observed "CTO" --correct "CEO"
200
218
  ```
201
219
 
202
220
  ### Credits
@@ -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 across 1.5B+ contacts and 200M+ companies 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
@@ -126,13 +126,20 @@ datalegion-cli person enrich --email john@example.com --multiple --min-confidenc
126
126
  datalegion-cli person enrich --email john@example.com --fields "full_name,emails,phones"
127
127
  ```
128
128
 
129
- ### Person search
129
+ ### Person search (Beta)
130
130
 
131
131
  ```bash
132
132
  # SQL-based search
133
133
  datalegion-cli person search "SELECT * FROM people WHERE company_name ILIKE '%google%' AND state = 'CA'" --limit 5
134
134
  ```
135
135
 
136
+ ### Person discovery (Beta)
137
+
138
+ ```bash
139
+ # Natural-language search
140
+ datalegion-cli person discover "software engineers in San Francisco who know Rust" --limit 10
141
+ ```
142
+
136
143
  ### Company enrichment
137
144
 
138
145
  ```bash
@@ -146,13 +153,20 @@ datalegion-cli company enrich --ticker AAPL
146
153
  datalegion-cli company enrich --name "Stripe" --industry "Financial Services"
147
154
  ```
148
155
 
149
- ### Company search
156
+ ### Company search (Beta)
150
157
 
151
158
  ```bash
152
159
  # SQL-based search
153
160
  datalegion-cli company search "SELECT * FROM companies WHERE industry ILIKE '%software%' AND legion_employee_count > 100"
154
161
  ```
155
162
 
163
+ ### Company discovery (Beta)
164
+
165
+ ```bash
166
+ # Natural-language search
167
+ datalegion-cli company discover "fintech startups in New York with under 50 employees"
168
+ ```
169
+
156
170
  ### Utility commands
157
171
 
158
172
  ```bash
@@ -164,6 +178,10 @@ datalegion-cli utility hash john@example.com
164
178
 
165
179
  # Validate fields before enrichment
166
180
  datalegion-cli utility validate --email "test@example.com" --phone "+15551234567"
181
+
182
+ # Report a data issue on a record (no credit cost; include --correct or --comment)
183
+ datalegion-cli utility report person --legion-id abc123 --issue-type outdated \
184
+ --issue-level field --field job_title --observed "CTO" --correct "CEO"
167
185
  ```
168
186
 
169
187
  ### Credits
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datalegion-cli"
3
- version = "1.1.0"
3
+ version = "1.2.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"
@@ -111,8 +111,8 @@ class LegionClient:
111
111
  async def person_search(self, **kwargs: Any) -> dict[str, Any]:
112
112
  return await self.request("POST", "/person/search", payload=_strip(kwargs))
113
113
 
114
- # async def person_discover(self, **kwargs: Any) -> dict[str, Any]:
115
- # return await self.request("POST", "/person/discover", payload=_strip(kwargs))
114
+ async def person_discover(self, **kwargs: Any) -> dict[str, Any]:
115
+ return await self.request("POST", "/person/discover", payload=_strip(kwargs))
116
116
 
117
117
  async def person_bulk(self, **kwargs: Any) -> dict[str, Any]:
118
118
  return await self.request("POST", "/person/enrich/bulk", payload=_strip(kwargs))
@@ -123,8 +123,8 @@ class LegionClient:
123
123
  async def company_search(self, **kwargs: Any) -> dict[str, Any]:
124
124
  return await self.request("POST", "/company/search", payload=_strip(kwargs))
125
125
 
126
- # async def company_discover(self, **kwargs: Any) -> dict[str, Any]:
127
- # return await self.request("POST", "/company/discover", payload=_strip(kwargs))
126
+ async def company_discover(self, **kwargs: Any) -> dict[str, Any]:
127
+ return await self.request("POST", "/company/discover", payload=_strip(kwargs))
128
128
 
129
129
  async def company_bulk(self, **kwargs: Any) -> dict[str, Any]:
130
130
  return await self.request("POST", "/company/enrich/bulk", payload=_strip(kwargs))
@@ -138,6 +138,9 @@ class LegionClient:
138
138
  async def utility_validate(self, **kwargs: Any) -> dict[str, Any]:
139
139
  return await self.request("POST", "/utility/validate", payload=_strip(kwargs))
140
140
 
141
+ async def utility_report(self, kind: str, **kwargs: Any) -> dict[str, Any]:
142
+ return await self.request("POST", f"/utility/report/{kind}", payload=_strip(kwargs))
143
+
141
144
  async def credits_balance(self) -> dict[str, Any]:
142
145
  return await self.request("GET", "/credits")
143
146
 
@@ -1,4 +1,4 @@
1
- """Company enrichment and search commands."""
1
+ """Company enrichment, search, and discovery commands."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -12,7 +12,7 @@ from datalegion_cli.async_typer import async_command
12
12
  from datalegion_cli.client import APIError, LegionClient
13
13
  from datalegion_cli.output import emit_credits, emit_error, emit_json
14
14
 
15
- app = typer.Typer(name="company", help="Company enrichment and search.")
15
+ app = typer.Typer(name="company", help="Company enrichment, search, and discovery.")
16
16
 
17
17
  # ── Shared options ───────────────────────────────────────────────────
18
18
 
@@ -110,7 +110,7 @@ async def search(
110
110
  include_fields: FieldsInclude = None,
111
111
  exclude_fields: FieldsExclude = None,
112
112
  ) -> None:
113
- """Search for companies using SQL WHERE syntax.
113
+ """Search for companies using SQL WHERE syntax (Beta).
114
114
 
115
115
  Examples:
116
116
  datalegion-cli company search "industry = 'Software' AND employee_count > 100"
@@ -135,30 +135,32 @@ async def search(
135
135
  await client.close()
136
136
 
137
137
 
138
- # TODO: Uncomment when discover is ready
139
- # @app.command()
140
- # @async_command
141
- # async def discover(
142
- # query: Annotated[str, typer.Argument(help="Natural language company search query.")],
143
- # limit: LimitOpt = 10,
144
- # titlecase: TitleOpt = False,
145
- # include_fields: FieldsInclude = None,
146
- # exclude_fields: FieldsExclude = None,
147
- # ) -> None:
148
- # """Search for companies using natural language."""
149
- # client = LegionClient()
150
- # try:
151
- # result = await client.company_discover(
152
- # query=query, limit=limit, titlecase=titlecase,
153
- # include_fields=include_fields, exclude_fields=exclude_fields,
154
- # )
155
- # emit_json(result)
156
- # emit_credits(client.last_credits_used, client.last_credits_remaining)
157
- # except APIError as e:
158
- # emit_error(e.message, code=e.error, details=e.details, as_json=True)
159
- # raise typer.Exit(1) from e
160
- # finally:
161
- # await client.close()
138
+ @app.command()
139
+ @async_command
140
+ async def discover(
141
+ query: Annotated[str, typer.Argument(help="Natural language company search query.")],
142
+ limit: LimitOpt = 10,
143
+ titlecase: TitleOpt = False,
144
+ include_fields: FieldsInclude = None,
145
+ exclude_fields: FieldsExclude = None,
146
+ ) -> None:
147
+ """Search for companies using natural language (Beta)."""
148
+ client = LegionClient()
149
+ try:
150
+ result = await client.company_discover(
151
+ query=query,
152
+ limit=limit,
153
+ titlecase=titlecase,
154
+ include_fields=include_fields,
155
+ exclude_fields=exclude_fields,
156
+ )
157
+ emit_json(result)
158
+ emit_credits(client.last_credits_used, client.last_credits_remaining)
159
+ except APIError as e:
160
+ emit_error(e.message, code=e.error, details=e.details, as_json=True)
161
+ raise typer.Exit(1) from e
162
+ finally:
163
+ await client.close()
162
164
 
163
165
 
164
166
  # TODO: Uncomment when bulk enrichment is ready
@@ -1,4 +1,4 @@
1
- """Person enrichment and search commands."""
1
+ """Person enrichment, search, and discovery commands."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -12,7 +12,7 @@ from datalegion_cli.async_typer import async_command
12
12
  from datalegion_cli.client import APIError, LegionClient
13
13
  from datalegion_cli.output import emit_credits, emit_error, emit_json
14
14
 
15
- app = typer.Typer(name="person", help="Person enrichment and search.")
15
+ app = typer.Typer(name="person", help="Person enrichment, search, and discovery.")
16
16
 
17
17
  # ── Shared options ───────────────────────────────────────────────────
18
18
 
@@ -136,7 +136,7 @@ async def search(
136
136
  include_fields: FieldsInclude = None,
137
137
  exclude_fields: FieldsExclude = None,
138
138
  ) -> None:
139
- """Search for people using SQL WHERE syntax.
139
+ """Search for people using SQL WHERE syntax (Beta).
140
140
 
141
141
  Examples:
142
142
  datalegion-cli person search "job_title = 'CEO' AND company = 'Acme Corp'"
@@ -161,30 +161,32 @@ async def search(
161
161
  await client.close()
162
162
 
163
163
 
164
- # TODO: Uncomment when discover is ready
165
- # @app.command()
166
- # @async_command
167
- # async def discover(
168
- # query: Annotated[str, typer.Argument(help="Natural language search query.")],
169
- # limit: LimitOpt = 10,
170
- # titlecase: TitleOpt = False,
171
- # include_fields: FieldsInclude = None,
172
- # exclude_fields: FieldsExclude = None,
173
- # ) -> None:
174
- # """Search for people using natural language."""
175
- # client = LegionClient()
176
- # try:
177
- # result = await client.person_discover(
178
- # query=query, limit=limit, titlecase=titlecase,
179
- # include_fields=include_fields, exclude_fields=exclude_fields,
180
- # )
181
- # emit_json(result)
182
- # emit_credits(client.last_credits_used, client.last_credits_remaining)
183
- # except APIError as e:
184
- # emit_error(e.message, code=e.error, details=e.details, as_json=True)
185
- # raise typer.Exit(1) from e
186
- # finally:
187
- # await client.close()
164
+ @app.command()
165
+ @async_command
166
+ async def discover(
167
+ query: Annotated[str, typer.Argument(help="Natural language search query.")],
168
+ limit: LimitOpt = 10,
169
+ titlecase: TitleOpt = False,
170
+ include_fields: FieldsInclude = None,
171
+ exclude_fields: FieldsExclude = None,
172
+ ) -> None:
173
+ """Search for people using natural language (Beta)."""
174
+ client = LegionClient()
175
+ try:
176
+ result = await client.person_discover(
177
+ query=query,
178
+ limit=limit,
179
+ titlecase=titlecase,
180
+ include_fields=include_fields,
181
+ exclude_fields=exclude_fields,
182
+ )
183
+ emit_json(result)
184
+ emit_credits(client.last_credits_used, client.last_credits_remaining)
185
+ except APIError as e:
186
+ emit_error(e.message, code=e.error, details=e.details, as_json=True)
187
+ raise typer.Exit(1) from e
188
+ finally:
189
+ await client.close()
188
190
 
189
191
 
190
192
  # TODO: Uncomment when bulk enrichment is ready
@@ -1,4 +1,4 @@
1
- """Utility commands — clean, hash, validate."""
1
+ """Utility commands — clean, hash, validate, report."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -12,7 +12,7 @@ from datalegion_cli.async_typer import async_command
12
12
  from datalegion_cli.client import APIError, LegionClient
13
13
  from datalegion_cli.output import emit_error, emit_json
14
14
 
15
- app = typer.Typer(name="utility", help="Data cleaning, hashing, and validation.")
15
+ app = typer.Typer(name="utility", help="Data cleaning, hashing, validation, and issue reporting.")
16
16
 
17
17
 
18
18
  @app.command()
@@ -120,3 +120,69 @@ async def validate(
120
120
  raise typer.Exit(1) from e
121
121
  finally:
122
122
  await client.close()
123
+
124
+
125
+ @app.command()
126
+ @async_command
127
+ async def report(
128
+ kind: Annotated[str, typer.Argument(help="Record type: person or company.")],
129
+ legion_id: Annotated[str, typer.Option("--legion-id", help="Legion ID of the record.")],
130
+ issue_type: Annotated[
131
+ str,
132
+ typer.Option(
133
+ "--issue-type",
134
+ help=(
135
+ "Issue type: incorrect, outdated, misattributed, frankenstein, "
136
+ "not_a_real_person (person only), or duplicate."
137
+ ),
138
+ ),
139
+ ],
140
+ issue_level: Annotated[
141
+ str,
142
+ typer.Option("--issue-level", help="Issue level: record, group, or field."),
143
+ ] = "record",
144
+ field: Annotated[
145
+ str | None,
146
+ typer.Option("--field", help="Group or field name (for group/field level issues)."),
147
+ ] = None,
148
+ observed_value: Annotated[
149
+ str | None, typer.Option("--observed", help="The incorrect value you observed.")
150
+ ] = None,
151
+ correct_value: Annotated[
152
+ str | None, typer.Option("--correct", help="The correct value, if known.")
153
+ ] = None,
154
+ comment: Annotated[
155
+ str | None, typer.Option("--comment", help="Free-form description of the issue.")
156
+ ] = None,
157
+ ) -> None:
158
+ """Report a data issue on a person or company record (no credit cost).
159
+
160
+ Include at least one of --correct or --comment.
161
+
162
+ Examples:
163
+ datalegion-cli utility report person --legion-id abc123 --issue-type outdated \\
164
+ --issue-level field --field job_title --observed "CTO" --correct "CEO"
165
+ datalegion-cli utility report company --legion-id xyz789 --issue-type duplicate \\
166
+ --comment "Same company as legion_id def456"
167
+ """
168
+ if kind not in ("person", "company"):
169
+ raise typer.BadParameter("kind must be 'person' or 'company'.")
170
+
171
+ client = LegionClient()
172
+ try:
173
+ result = await client.utility_report(
174
+ kind,
175
+ legion_id=legion_id,
176
+ issue_type=issue_type,
177
+ issue_level=issue_level,
178
+ field=field,
179
+ observed_value=observed_value,
180
+ correct_value=correct_value,
181
+ comment=comment,
182
+ )
183
+ emit_json(result)
184
+ except APIError as e:
185
+ emit_error(e.message, code=e.error, details=e.details, as_json=True)
186
+ raise typer.Exit(1) from e
187
+ finally:
188
+ await client.close()