pingintel-api 1.4.6__tar.gz → 1.4.8__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 (59) hide show
  1. pingintel_api-1.4.8/.claude/settings.json +7 -0
  2. pingintel_api-1.4.8/.claude/settings.local.json +16 -0
  3. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/PKG-INFO +1 -1
  4. pingintel_api-1.4.8/examples/close_submissions.py +129 -0
  5. pingintel_api-1.4.8/src/pingintel_api/__about__.py +1 -0
  6. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingdata/pingdata_api_client.py +19 -1
  7. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingdata/types.py +11 -7
  8. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingvisionapi_cmd.py +46 -0
  9. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/sov_fixer/sov_fixer_api_client.py +151 -30
  10. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/sov_fixer/types.py +49 -0
  11. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/sovfixerapi_cmd.py +19 -0
  12. pingintel_api-1.4.6/src/pingintel_api/__about__.py +0 -1
  13. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/.gitignore +0 -0
  14. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/.pre-commit-config.yaml +0 -0
  15. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/LICENSE +0 -0
  16. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/README.md +0 -0
  17. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/SOV10 2026-02_Acc-STG.txt +0 -0
  18. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/Untitled-9.json5 +0 -0
  19. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/assignment_queue.xlsx +0 -0
  20. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/completed_submissions.xlsx +0 -0
  21. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/data/demo_email.eml +0 -0
  22. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/data/parse_sov_testfile.xlsx +0 -0
  23. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/find-amwins-missing-account-numbers.py +0 -0
  24. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-data/enhance_multiple_locations.py +0 -0
  25. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-data/enhance_one_location.py +0 -0
  26. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-data/pingdata_enhance.py +0 -0
  27. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-extraction/parse_sov.py +0 -0
  28. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-extraction/sovfixer_fix_sov.py +0 -0
  29. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-extraction/sovfixer_list_activity.py +0 -0
  30. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-extraction/sovfixer_reoutput_sov.py +0 -0
  31. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-vision/clear_and_triage_api.py +0 -0
  32. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-vision/pingvision_activity.py +0 -0
  33. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-vision/pingvision_clear_and_triage.py +0 -0
  34. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/ping-vision/send_and_track_submission.py +0 -0
  35. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/pingvision_assignment.py +0 -0
  36. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/pingvision_declination_flow.py +0 -0
  37. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/sovfixer_list_history.py +0 -0
  38. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/sovfixer_list_history_ping_None_1_run2.csv +0 -0
  39. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/sovfixerapi +0 -0
  40. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/test_reoutput_locations.csv +0 -0
  41. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/examples/test_sov.xlsx +0 -0
  42. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/generate_docs.py +0 -0
  43. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/justfile +0 -0
  44. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/output-data-f432c75e-b40e-11f0-9e68-3c58c2f91432-1.json +0 -0
  45. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/pyproject.toml +0 -0
  46. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/requirements.txt +0 -0
  47. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/__init__.py +0 -0
  48. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/api_client_base.py +0 -0
  49. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/common_types.py +0 -0
  50. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/constants.py +0 -0
  51. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingdataapi_cmd.py +0 -0
  52. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingmaps/pingmaps_api_client.py +0 -0
  53. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingmaps/types.py +0 -0
  54. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingmapsapi_cmd.py +0 -0
  55. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingvision/pingvision_api_client.py +0 -0
  56. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/pingvision/types.py +0 -0
  57. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/url_signature_factory.py +0 -0
  58. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/src/pingintel_api/utils.py +0 -0
  59. {pingintel_api-1.4.6 → pingintel_api-1.4.8}/tests/__init__.py +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:data-api.sovfixer.com)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:data-api.sovfixer.com)",
5
+ "WebFetch(domain:docs.pingintel.com)",
6
+ "WebFetch(domain:api.sovfixer.com)",
7
+ "Bash(curl -s \"http://localhost/olddocs\")",
8
+ "Bash(curl -sv \"http://localhost/api/schema/\")",
9
+ "Bash(curl -s \"http://localhost/api/schema/\")",
10
+ "Bash(python3 -c ' *)",
11
+ "Bash(mkdir -p \"C:/temp\")",
12
+ "Bash(curl -s \"http://localhost/api/schema/\" -o \"C:/temp/schema.yaml\")",
13
+ "Read(//c/temp/**)"
14
+ ]
15
+ }
16
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingintel-api
3
- Version: 1.4.6
3
+ Version: 1.4.8
4
4
  Summary: Python-based client to Ping Data Intelligence APIs
5
5
  Project-URL: Homepage, https://github.com/pingintel/pingintel-api
6
6
  Project-URL: Issues, https://github.com/pingintel/pingintel-api/issues
@@ -0,0 +1,129 @@
1
+ """
2
+ Set one or more PingVision submissions to a given workflow status.
3
+
4
+ Accepts pingids (e.g. p-ob-amw-agtdgc) or client_refs. Identifiers starting
5
+ with "p-" are treated as pingids; all others are looked up as client_refs.
6
+
7
+ Usage:
8
+ python close_submissions.py <id1> [id2 ...] --status <name> [options]
9
+
10
+ python close_submissions.py p-ob-amw-abc123 my-client-ref-456 --status Closed
11
+ python close_submissions.py p-ob-amw-abc123 --status Declined --env staging --dry-run
12
+ """
13
+
14
+ import argparse
15
+ import sys
16
+
17
+ from pingintel_api import PingVisionAPIClient
18
+
19
+
20
+ def resolve_pingids(client: PingVisionAPIClient, identifiers: list[str]) -> dict[str, str | None]:
21
+ """Return {identifier: pingid} for all identifiers. pingid is None if not found."""
22
+ result = {}
23
+ for ident in identifiers:
24
+ if ident.startswith("p-"):
25
+ result[ident] = ident
26
+ else:
27
+ activity = client.list_submission_activity(client_ref=ident, page_size=10)
28
+ submissions = activity.get("results", [])
29
+ if not submissions:
30
+ print(f" WARNING: No submission found for client_ref={ident!r}", file=sys.stderr)
31
+ result[ident] = None
32
+ elif len(submissions) > 1:
33
+ pingids = [s["id"] for s in submissions]
34
+ print(
35
+ f" WARNING: Multiple submissions found for client_ref={ident!r}: {pingids}. Skipping.",
36
+ file=sys.stderr,
37
+ )
38
+ result[ident] = None
39
+ else:
40
+ result[ident] = submissions[0]["id"]
41
+ return result
42
+
43
+
44
+ def find_status_uuid(client: PingVisionAPIClient, division_uuid: str, status_name: str) -> str:
45
+ statuses = client.list_submission_statuses(division=division_uuid)
46
+ matched = [s for s in statuses if s["name"].lower() == status_name.lower()]
47
+ if not matched:
48
+ available = [s["name"] for s in statuses]
49
+ raise RuntimeError(f"Status {status_name!r} not found. Available: {available}")
50
+ return matched[0]["uuid"]
51
+
52
+
53
+ def find_division_uuid(client: PingVisionAPIClient, team_name: str | None, division: str | None) -> str:
54
+ teams = client.list_teams()
55
+ if not teams:
56
+ raise RuntimeError("No teams found for this API token.")
57
+ if division:
58
+ matched = [
59
+ t for t in teams
60
+ if t["division_uuid"] == division
61
+ or t.get("division_name", "").lower() == division.lower()
62
+ or t.get("division_short_name", "").lower() == division.lower()
63
+ ]
64
+ if not matched:
65
+ available = [(t.get("division_short_name") or t["division_name"], t["division_uuid"]) for t in teams]
66
+ raise RuntimeError(f"Division {division!r} not found. Available: {available}")
67
+ return matched[0]["division_uuid"]
68
+ if team_name:
69
+ matched = [t for t in teams if t["team_name"] == team_name]
70
+ if not matched:
71
+ available = [t["team_name"] for t in teams]
72
+ raise RuntimeError(f"Team {team_name!r} not found. Available: {available}")
73
+ return matched[0]["division_uuid"]
74
+ if len(teams) == 1:
75
+ return teams[0]["division_uuid"]
76
+ names = [t["team_name"] for t in teams]
77
+ raise RuntimeError(f"Multiple teams found: {names}. Specify one with --team or --division.")
78
+
79
+
80
+ def main():
81
+ parser = argparse.ArgumentParser(description="Set PingVision submission workflow status.")
82
+ parser.add_argument("identifiers", nargs="+", help="pingids or client_refs to update")
83
+ parser.add_argument("--status", required=True, help="Target workflow status name (e.g. Closed, Declined)")
84
+ parser.add_argument("--env", default="prod", help="API environment (prod, staging, local). Default: prod")
85
+ parser.add_argument("--api-url", help="Override API URL (e.g. http://localhost:8002)")
86
+ parser.add_argument("--team", help="Team name for status lookup (required if multiple teams)")
87
+ parser.add_argument("--division", help="Division UUID (alternative to --team)")
88
+ parser.add_argument("--dry-run", action="store_true", help="Print what would happen without making changes")
89
+ args = parser.parse_args()
90
+
91
+ kwargs = {}
92
+ if args.api_url:
93
+ kwargs["api_url"] = args.api_url
94
+ else:
95
+ kwargs["environment"] = args.env
96
+
97
+ client = PingVisionAPIClient(**kwargs)
98
+
99
+ division_uuid = find_division_uuid(client, args.team, args.division)
100
+ status_uuid = find_status_uuid(client, division_uuid, args.status)
101
+ print(f"Status {args.status!r} UUID: {status_uuid}")
102
+
103
+ id_map = resolve_pingids(client, args.identifiers)
104
+
105
+ success, skipped, failed = 0, 0, 0
106
+ for ident, pingid in id_map.items():
107
+ if pingid is None:
108
+ skipped += 1
109
+ continue
110
+ label = pingid if ident == pingid else f"{pingid} (client_ref={ident!r})"
111
+ if args.dry_run:
112
+ print(f" DRY RUN: would set {label} -> {args.status!r}")
113
+ success += 1
114
+ continue
115
+ try:
116
+ client.change_status(pingid=pingid, workflow_status_id=status_uuid)
117
+ print(f" Updated {label} -> {args.status!r}")
118
+ success += 1
119
+ except Exception as e:
120
+ print(f" ERROR updating {label}: {e}", file=sys.stderr)
121
+ failed += 1
122
+
123
+ print(f"\nDone. {success} updated, {skipped} skipped, {failed} failed.")
124
+ if failed:
125
+ sys.exit(1)
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
@@ -0,0 +1 @@
1
+ __version__ = "1.4.8"
@@ -162,7 +162,7 @@ class PingDataAPIClient(APIClientBase):
162
162
  delegate_to=delegate_to,
163
163
  )
164
164
  request_id = response_data["id"]
165
- message = response_data["message"]
165
+ message = response_data.get("message", "")
166
166
 
167
167
  self.logger.info(
168
168
  f"+ Dispatched {request_id}: {message}. Now, polling for results at {self.bulk_enhance_async_get_status_url(request_id=request_id)}."
@@ -299,3 +299,21 @@ class PingDataAPIClient(APIClientBase):
299
299
  raise_for_status(response)
300
300
  response_data = response.json()
301
301
  return response_data
302
+
303
+ def fetch_bulk_enhance_output(self, request_id: str, filename: str, output_path: str | None = None) -> bytes:
304
+ """
305
+ Download a result file from a completed bulk enhance job.
306
+
307
+ :param request_id: The bulk enhance job ID.
308
+ :param filename: The output filename (from result.outputs[].filename).
309
+ :param output_path: If provided, write the result to this local file path.
310
+ :return: Raw response bytes.
311
+ """
312
+ url = self.api_url + f"/api/v1/bulk_enhance/{request_id}/output/{filename}"
313
+ response = self.get(url)
314
+ raise_for_status(response)
315
+ if output_path:
316
+ with open(output_path, "wb") as fd:
317
+ for chunk in response.iter_content(chunk_size=128):
318
+ fd.write(chunk)
319
+ return response.content
@@ -124,9 +124,6 @@ class SingleLocation(TypedDict):
124
124
  address: NotRequired[str | None]
125
125
  latitude: NotRequired[float | None]
126
126
  longitude: NotRequired[float | None]
127
- limits__building_limit: NotRequired[float | None]
128
- limits__bpp_limit: NotRequired[float | None]
129
- limits__bi_limit: NotRequired[float | None]
130
127
  address_line_1: NotRequired[str | None]
131
128
  address_line_2: NotRequired[str | None]
132
129
  city: NotRequired[str | None]
@@ -135,9 +132,10 @@ class SingleLocation(TypedDict):
135
132
  country: NotRequired[str | None]
136
133
  county: NotRequired[str | None]
137
134
  bldg_name: NotRequired[str | None]
138
- # ping_occupancy_data: NotRequired[str | None]
139
- # llm_text_blob: NotRequired[str | None]
140
- # address_field_data: NotRequired[str | None]
135
+ lightbox_assessment_id: NotRequired[str | None]
136
+ lightbox_building_id: NotRequired[str | None]
137
+ lightbox_parcel_id: NotRequired[str | None]
138
+ lightbox_parcel_wkt: NotRequired[str | None]
141
139
  occupancy__type_desc: NotRequired[str | None]
142
140
  occupancy__desc_ping: NotRequired[str | None]
143
141
  occupancy__code_air: NotRequired[str | None]
@@ -149,10 +147,14 @@ class SingleLocation(TypedDict):
149
147
  const__wall_type: NotRequired[str | None]
150
148
  const__bldg_year_built: NotRequired[int | None]
151
149
  const__num_stories: NotRequired[int | None]
150
+ const__num_stories_below_gnd: NotRequired[int | None]
152
151
  const__bldg_area: NotRequired[int | None]
152
+ limits__building_limit: NotRequired[float | None]
153
+ limits__bpp_limit: NotRequired[float | None]
154
+ limits__bi_limit: NotRequired[float | None]
155
+ limits__signs_and_other_limit: NotRequired[float | None]
153
156
  dtc_include_coastline_within_miles: NotRequired[float | None]
154
157
  dtc_return_connected_coastlines: NotRequired[bool | None]
155
- insured_name: NotRequired[str | None]
156
158
 
157
159
 
158
160
  Location = SingleLocation
@@ -160,6 +162,8 @@ Location = SingleLocation
160
162
 
161
163
  class BatchLocation(SingleLocation):
162
164
  id: str
165
+ sources: NotRequired[list[str] | None]
166
+ datasource_config: NotRequired[dict | None]
163
167
 
164
168
 
165
169
  class EnhanceResponse(TypedDict):
@@ -316,6 +316,52 @@ def list_submission_statuses(ctx, division):
316
316
  pprint.pprint(results)
317
317
 
318
318
 
319
+ @cli.command()
320
+ @click.pass_context
321
+ @click.option("--pretty", is_flag=True, default=False)
322
+ @click.option("--pingid", help="Filter by Ping ID")
323
+ @click.option("--division", help="Filter by division UUID or short name")
324
+ @click.option("--team", help="Filter by team UUID or short name")
325
+ @click.option("--start", help="Start datetime filter (YYYYMMDDHHmmss UTC)")
326
+ @click.option("--cursor-id", help="Cursor ID for pagination")
327
+ @click.option("-l", "--page-size", "--limit", default=50)
328
+ def events(ctx, pretty, pingid, division, team, start, cursor_id, page_size):
329
+ """List submission events."""
330
+ import datetime
331
+
332
+ client = get_client(ctx)
333
+
334
+ if not any([pingid, division, team]):
335
+ raise click.UsageError("One or more of --pingid, --division, or --team must be provided.")
336
+
337
+ start_dt = None
338
+ if start:
339
+ start_dt = datetime.datetime.strptime(start, "%Y%m%d%H%M%S")
340
+
341
+ results = client.list_submission_events(
342
+ pingid=pingid,
343
+ division=division,
344
+ team=team,
345
+ start=start_dt,
346
+ cursor_id=cursor_id,
347
+ page_size=page_size,
348
+ )
349
+ if pretty:
350
+ print(f"{'Ping ID':<20}{'Event Type':<6}{'Created':<20}{'Message'}")
351
+ for event in results["results"]:
352
+ created = event["created_time"]
353
+ try:
354
+ created = time.strftime("%Y-%m-%d %H:%M", time.strptime(created, "%Y-%m-%dT%H:%M:%S.%fZ"))
355
+ except ValueError:
356
+ pass
357
+ print(f"{event['pingid']:<20}{event['event_type']:<6}{created:<20}{event.get('message', '')}")
358
+ cursor = results.get("cursor_id")
359
+ if cursor:
360
+ print(f"\nNext cursor: {cursor}")
361
+ else:
362
+ pprint.pprint(results)
363
+
364
+
319
365
  def main():
320
366
  cli()
321
367
 
@@ -42,6 +42,8 @@ class SOVFixerAPIClient(APIClientBase):
42
42
  allow_ping_data_api=None,
43
43
  workflow=None,
44
44
  skip_prior_update_reuse: bool = False,
45
+ company: str | None = None,
46
+ team: str | None = None,
45
47
  ):
46
48
  """
47
49
  Start a SOV Fixer request from one or more files asynchronously.
@@ -77,6 +79,10 @@ class SOVFixerAPIClient(APIClientBase):
77
79
  data["allow_ping_data_api"] = allow_ping_data_api
78
80
  if workflow is not None:
79
81
  data["workflow"] = workflow
82
+ if company is not None:
83
+ data["company"] = company
84
+ if team is not None:
85
+ data["team"] = team
80
86
 
81
87
  data["skip_prior_update_reuse"] = skip_prior_update_reuse
82
88
 
@@ -250,7 +256,7 @@ class SOVFixerAPIClient(APIClientBase):
250
256
  if request_status == "PENDING":
251
257
  self.logger.info(" - Has not yet been queued for processing.")
252
258
  time.sleep(POLL_SECS)
253
- elif request_status == "IN_PROGRESS":
259
+ elif request_status in t.INCOMPLETE_STATUSES:
254
260
  self.logger.info(f" - Still in progress ({pct_complete}% complete): {last_status}")
255
261
  time.sleep(POLL_SECS)
256
262
  else:
@@ -340,27 +346,28 @@ class SOVFixerAPIClient(APIClientBase):
340
346
  search=None,
341
347
  origin: Literal["api", "email"] | None = None,
342
348
  status: Literal["P", "I", "E", "R", "C", "F"] | None = None,
343
- organization__short_name=None,
349
+ company__short_name: str | list[str] | None = None,
350
+ division__short_name: str | list[str] | None = None,
351
+ pingid: str | None = None,
352
+ completed_time__gt: str | None = None,
353
+ completed_time__gte: str | None = None,
354
+ completed_time__lt: str | None = None,
355
+ completed_time__lte: str | None = None,
344
356
  ) -> t.ActivityResponse:
345
357
  """List activity in the SOV Fixer system.
346
- id: str
347
- The ID of the activity to retrieve.
348
- cursor_id: str
349
- The cursor ID to use for pagination. Do not set on the first call, but provide the value from each previous call to the next to get the next page.
350
- prev_cursor_id: str
351
- See cursor_id, but this goes backwards.
352
- page_size: int
353
- The number of results to return per page. Default is 50.
354
- fields: str
355
- The fields to include in the response. Default is all fields.
356
- search: str
357
- A search term to filter results by.
358
- origin: str
359
- Filter by the origin of the activity. Can be "api" or "email".
360
- status: str
361
- Filter by the status of the activity. Can be "P" (pending), "I" (in progress), "E" (enriching), "R" (re-enriching), "C" (complete), or "F" (failed).
362
- organization__short_name: str
363
- Filter by the short name of the organization that created the activity
358
+
359
+ id: filter by sovid.
360
+ cursor_id: pagination cursor from a previous response.
361
+ prev_cursor_id: reverse pagination cursor.
362
+ page_size: max results per page (default 50, max 250).
363
+ fields: list of field names to include on each result.
364
+ search: global case-insensitive search substring.
365
+ origin: filter by "api" or "email".
366
+ status: filter by status
367
+ company__short_name: filter by company short name(s).
368
+ division__short_name: filter by division short name(s).
369
+ pingid: filter by Ping ID.
370
+ completed_time__gt/gte/lt/lte: filter by completed_time (format: YYYYMMDDHHmmss, UTC).
364
371
  """
365
372
  parameters = {}
366
373
  if id:
@@ -379,8 +386,20 @@ class SOVFixerAPIClient(APIClientBase):
379
386
  parameters["origin"] = origin
380
387
  if status:
381
388
  parameters["status"] = status
382
- if organization__short_name:
383
- parameters["organization__short_name"] = organization__short_name
389
+ if company__short_name:
390
+ parameters["company__short_name"] = company__short_name
391
+ if division__short_name:
392
+ parameters["division__short_name"] = division__short_name
393
+ if pingid:
394
+ parameters["pingid"] = pingid
395
+ if completed_time__gt:
396
+ parameters["completed_time__gt"] = completed_time__gt
397
+ if completed_time__gte:
398
+ parameters["completed_time__gte"] = completed_time__gte
399
+ if completed_time__lt:
400
+ parameters["completed_time__lt"] = completed_time__lt
401
+ if completed_time__lte:
402
+ parameters["completed_time__lte"] = completed_time__lte
384
403
 
385
404
  url = self.api_url + "/api/v1/sov/activity"
386
405
  response = self.get(url, params=parameters)
@@ -464,20 +483,19 @@ class SOVFixerAPIClient(APIClientBase):
464
483
  sudid: str,
465
484
  extra_data=None,
466
485
  policy_terms=None,
467
- policy_terms_format_name=None,
486
+ outputter_name: str | None = None,
468
487
  output_formats=None,
469
488
  metadata=None,
470
489
  integrations=None,
471
490
  delegate_to_team: UUID | str | int | None = None,
472
491
  ) -> t.SOVUpdateAsyncAPIResponse:
473
492
  url = self.api_url + f"/api/v1/sov/update/{sudid}/start"
474
- data = {}
475
- if extra_data:
476
- data["extra_data"] = extra_data
493
+ data: dict = {}
494
+ data["extra_data"] = extra_data or {}
477
495
  if policy_terms:
478
496
  data["policy_terms"] = policy_terms
479
- if policy_terms_format_name:
480
- data["policy_terms_format_name"] = policy_terms_format_name
497
+ if outputter_name:
498
+ data["outputter_name"] = outputter_name
481
499
  if output_formats:
482
500
  data["output_formats"] = output_formats
483
501
  if metadata:
@@ -515,7 +533,7 @@ class SOVFixerAPIClient(APIClientBase):
515
533
  location_filenames,
516
534
  extra_data=None,
517
535
  policy_terms=None,
518
- policy_terms_format_name=None,
536
+ outputter_name: str | None = None,
519
537
  output_formats=None,
520
538
  actually_write=False,
521
539
  update_type=None,
@@ -545,7 +563,7 @@ class SOVFixerAPIClient(APIClientBase):
545
563
  sudid,
546
564
  extra_data=extra_data,
547
565
  policy_terms=policy_terms,
548
- policy_terms_format_name=policy_terms_format_name,
566
+ outputter_name=outputter_name,
549
567
  output_formats=output_formats,
550
568
  metadata=metadata,
551
569
  integrations=integrations,
@@ -625,6 +643,7 @@ class SOVFixerAPIClient(APIClientBase):
625
643
  def get_or_create_output_async_check_progress(self, output_request_id: str):
626
644
  url = self.api_url + f"/api/v1/sov/get_or_create_output/{output_request_id}"
627
645
  response = self.get(url)
646
+ raise_for_status(response)
628
647
  return response.json()
629
648
 
630
649
  def get_or_create_output(
@@ -697,3 +716,105 @@ class SOVFixerAPIClient(APIClientBase):
697
716
 
698
717
  response_data = response.json()
699
718
  return response_data
719
+
720
+ def fetch_sov_output(self, sov_id: str, filename: str, output_path: str | None = None) -> bytes:
721
+ """
722
+ Download an output file from a completed SOV parsing job.
723
+
724
+ :param sov_id: The SOV job ID.
725
+ :param filename: The output filename (from result.outputs[].filename).
726
+ :param output_path: If provided, write the result to this local file path.
727
+ :return: Raw response bytes.
728
+ """
729
+ url = self.api_url + f"/api/v1/sov/{sov_id}/output/{filename}"
730
+ response = self.get(url)
731
+ raise_for_status(response)
732
+ if output_path:
733
+ with open(output_path, "wb") as fd:
734
+ for chunk in response.iter_content(chunk_size=1024 * 1024):
735
+ fd.write(chunk)
736
+ return response.content
737
+
738
+ def get_history_item(self, id: str) -> t.SOVHistoryResponse:
739
+ """
740
+ Get a specific historical SOV or SOV Update by its ID.
741
+
742
+ :param id: SOV ID (sovid) or SOV Update ID (sudid).
743
+ :return: Historical SOV data including outputs.
744
+ """
745
+ url = self.api_url + f"/api/v1/sov/history/{id}"
746
+ response = self.get(url)
747
+ raise_for_status(response)
748
+ return response.json()
749
+
750
+ def get_building(self, item_key: str) -> dict:
751
+ """
752
+ Retrieve a specific building by its item key.
753
+
754
+ :param item_key: Unique building identifier, e.g. "i-s-e-xxxxxxx!SOV!1".
755
+ :return: Building data dict.
756
+ """
757
+ url = self.api_url + f"/api/v1/building/{item_key}"
758
+ response = self.get(url)
759
+ raise_for_status(response)
760
+ return response.json()
761
+
762
+ def list_output_formats(
763
+ self,
764
+ sovid: str | None = None,
765
+ division_uuid: str | None = None,
766
+ team_uuid: str | None = None,
767
+ ) -> t.OutputFormatsResponse:
768
+ """List available output formats for the given context."""
769
+ url = self.api_url + "/api/v1/output_formats"
770
+ params = {}
771
+ if sov_id:
772
+ params["sovid"] = sovid
773
+ if division_uuid:
774
+ params["division_uuid"] = division_uuid
775
+ if team_uuid:
776
+ params["team_uuid"] = team_uuid
777
+ response = self.get(url, params=params)
778
+ raise_for_status(response)
779
+ return response.json()
780
+
781
+ def get_public_shareable_url(self, sovid: str) -> t.GetPublicShareableUrlResponse:
782
+ """
783
+ Get a publicly shareable Ping.Maps URL for the given SOV.
784
+
785
+ :param sovid: The SOV ID.
786
+ :return: Dict with a "url" key containing the shareable URL.
787
+ """
788
+ url = self.api_url + f"/api/v1/pli/policy/{sovid}/get_public_shareable_url"
789
+ response = self.get(url)
790
+ raise_for_status(response)
791
+ return response.json()
792
+
793
+ def create_submission(
794
+ self,
795
+ document_type: str = "SOV",
796
+ client_ref: str | None = None,
797
+ extra_data: dict | None = None,
798
+ filename: str | None = None,
799
+ ) -> t.CreateSubmissionResponse:
800
+ """
801
+ Create a new empty submission. Follow with update_sov_async_add_locations then update_sov_async_start.
802
+
803
+ :param document_type: "SOV", "PREM_BDX", "CLAIM_BDX", or "SOV_BDX". Default "SOV".
804
+ :param client_ref: Optional user reference identifier.
805
+ :param extra_data: Optional dict of extra_data_* fields (e.g. insured_name, inception_date).
806
+ :param filename: Optional filename for the submission.
807
+ :return: Dict with "id" (int) and "message".
808
+ """
809
+ url = self.api_url + "/api/v1/submission"
810
+ data: dict = {"document_type": document_type}
811
+ if client_ref:
812
+ data["client_ref"] = client_ref
813
+ if extra_data:
814
+ for k, v in extra_data.items():
815
+ data["extra_data_" + k] = v
816
+ if filename:
817
+ data["filename"] = filename
818
+ response = self.post(url, data=data)
819
+ raise_for_status(response)
820
+ return response.json()
@@ -83,6 +83,7 @@ class SOVUpdateInitiateRequest(TypedDict):
83
83
  update_type: NotRequired[str | None]
84
84
  callback_url: NotRequired[str | None]
85
85
  username: NotRequired[str | None]
86
+ delegate_to_team: NotRequired[str | int | None]
86
87
 
87
88
 
88
89
  class SOVUpdateResponseResultOutputs(TypedDict):
@@ -165,6 +166,7 @@ class SOVData(TypedDict):
165
166
  document_type: str | None
166
167
  pct_complete: float | None
167
168
  filename: str | None
169
+ additional_filenames: list[str] | None
168
170
  status: str | None
169
171
  status_display: str | None
170
172
  last_health_status: str | None
@@ -173,12 +175,17 @@ class SOVData(TypedDict):
173
175
  output_data: list[OutputData] | None
174
176
  origin: str | None
175
177
  organization__short_name: str | None
178
+ company__short_name: str | None
179
+ team__name: str | None
180
+ division__short_name: str | None
176
181
  global_request_id: str | None
177
182
  ping_maps_url: str | None
183
+ ping_vision_url: str | None
178
184
  input_file_url: str | None
179
185
  extra_data: dict[str, Any] | None
180
186
  updates: dict[str, UpdateData] | None
181
187
  all_updates: list[UpdateData] | None
188
+ latest_revision: int | None
182
189
  completed_time: str | None
183
190
  created_time: str | None
184
191
  subject: str | None
@@ -198,6 +205,9 @@ class ActivityResponse(TypedDict):
198
205
  cursor_id: str | None
199
206
  prev_cursor_id: str | None
200
207
  remaining_count: int
208
+ has_remaining: bool
209
+ total_count: int
210
+ total_size_without_cursors: str
201
211
 
202
212
 
203
213
  class HistoryItem(TypedDict):
@@ -210,8 +220,47 @@ class HistoryItem(TypedDict):
210
220
  completed_time: datetime
211
221
  record_type: str
212
222
  incremental: bool
223
+ is_data_ready: NotRequired[bool | None]
213
224
 
214
225
 
215
226
  class HistoryResponse(TypedDict):
216
227
  cursor_id: str | None
217
228
  results: list[HistoryItem]
229
+
230
+
231
+ class SOVHistoryOutputItem(TypedDict):
232
+ url: str
233
+ output_format: str
234
+ completed_time: str
235
+
236
+
237
+ class SOVHistoryResult(TypedDict):
238
+ sovid: str
239
+ previous_sovid: str
240
+ original_sovid: str
241
+ sheet_name: str
242
+ outputs: list[SOVHistoryOutputItem]
243
+
244
+
245
+ class SOVHistoryResponse(TypedDict):
246
+ result: SOVHistoryResult
247
+
248
+
249
+ class CreateSubmissionResponse(TypedDict):
250
+ message: str
251
+ id: int
252
+
253
+
254
+ class GetPublicShareableUrlResponse(TypedDict):
255
+ url: str
256
+
257
+
258
+ class OutputFormatItem(TypedDict):
259
+ output_format: str
260
+ label: str
261
+ is_public: bool
262
+ output_extension: str | None
263
+
264
+
265
+ class OutputFormatsResponse(TypedDict):
266
+ output_formats: list[OutputFormatItem]
@@ -502,6 +502,25 @@ def update(
502
502
  click.echo(f"Executed SOV Update, SUDID: {sudid}")
503
503
 
504
504
 
505
+ @cli.command()
506
+ @click.pass_context
507
+ @click.option("--sovid", help="Scope to a specific SOV ID")
508
+ @click.option("--division-uuid", help="Scope to a division UUID")
509
+ @click.option("--team-uuid", help="Scope to a team UUID")
510
+ @click.option("--pretty", is_flag=True, default=False)
511
+ def list_output_formats(ctx, sovid, division_uuid, team_uuid, pretty):
512
+ """List available output formats."""
513
+ client = get_client(ctx)
514
+ results = client.list_output_formats(sovid=sovid, division_uuid=division_uuid, team_uuid=team_uuid)
515
+ if pretty:
516
+ print(f"{'Format':<30}{'Label':<40}{'Extension':<12}{'Public'}")
517
+ for fmt in results["output_formats"]:
518
+ ext = fmt.get("output_extension") or ""
519
+ print(f"{fmt['output_format']:<30}{fmt['label']:<40}{ext:<12}{fmt['is_public']}")
520
+ else:
521
+ pprint.pprint(results)
522
+
523
+
505
524
  def main():
506
525
  cli()
507
526
 
@@ -1 +0,0 @@
1
- __version__ = "1.4.6"
File without changes
File without changes
File without changes
File without changes