pingintel-api 1.4.10__tar.gz → 1.4.11__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 (52) hide show
  1. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/.gitignore +2 -1
  2. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/PKG-INFO +16 -11
  3. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/README.md +14 -10
  4. pingintel_api-1.4.11/requirements.txt +3 -0
  5. pingintel_api-1.4.11/src/pingintel_api/__about__.py +1 -0
  6. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingdata/pingdata_api_client.py +47 -0
  7. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingdata/types.py +42 -2
  8. pingintel_api-1.4.11/src/pingintel_api/pingdataapi_cmd.py +449 -0
  9. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingvision/pingvision_api_client.py +85 -0
  10. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingvisionapi_cmd.py +56 -0
  11. pingintel_api-1.4.10/requirements.txt +0 -2
  12. pingintel_api-1.4.10/src/pingintel_api/__about__.py +0 -1
  13. pingintel_api-1.4.10/src/pingintel_api/pingdataapi_cmd.py +0 -263
  14. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/.pre-commit-config.yaml +0 -0
  15. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/LICENSE +0 -0
  16. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/data/corrections.csv +0 -0
  17. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/data/demo_email.eml +0 -0
  18. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/data/parse_sov_testfile.xlsx +0 -0
  19. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-data/enhance_multiple_locations.py +0 -0
  20. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-data/enhance_one_location.py +0 -0
  21. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-data/pingdata_enhance.py +0 -0
  22. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/build_sov_pipeline.py +0 -0
  23. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/get_or_create_sov_output.py +0 -0
  24. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/monitor_sov_activity.py +0 -0
  25. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/parse_sov.py +0 -0
  26. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/sovfixer_fix_sov.py +0 -0
  27. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/sovfixer_list_activity.py +0 -0
  28. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/sovfixer_reoutput_sov.py +0 -0
  29. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-extraction/update_sov.py +0 -0
  30. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-vision/clear_and_triage_api.py +0 -0
  31. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-vision/pingvision_activity.py +0 -0
  32. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/ping-vision/send_and_track_submission.py +0 -0
  33. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/pingvision_assignment.py +0 -0
  34. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/test_reoutput_locations.csv +0 -0
  35. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/examples/test_sov.xlsx +0 -0
  36. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/generate_docs.py +0 -0
  37. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/justfile +0 -0
  38. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/pyproject.toml +0 -0
  39. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/__init__.py +0 -0
  40. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/api_client_base.py +0 -0
  41. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/common_types.py +0 -0
  42. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/constants.py +0 -0
  43. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingmaps/pingmaps_api_client.py +0 -0
  44. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingmaps/types.py +0 -0
  45. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingmapsapi_cmd.py +0 -0
  46. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/pingvision/types.py +0 -0
  47. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/sov_fixer/sov_fixer_api_client.py +0 -0
  48. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/sov_fixer/types.py +0 -0
  49. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/sovfixerapi_cmd.py +0 -0
  50. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/url_signature_factory.py +0 -0
  51. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/src/pingintel_api/utils.py +0 -0
  52. {pingintel_api-1.4.10 → pingintel_api-1.4.11}/tests/__init__.py +0 -0
@@ -163,4 +163,5 @@ release.bat
163
163
  /_version.py
164
164
 
165
165
  .DS_Store
166
- *.bak
166
+ *.bak
167
+ .python-version
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingintel-api
3
- Version: 1.4.10
3
+ Version: 1.4.11
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
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.11
13
13
  Requires-Dist: click
14
14
  Requires-Dist: requests
15
+ Requires-Dist: typing-extensions>=4.13
15
16
  Description-Content-Type: text/markdown
16
17
 
17
18
  # pingintel-api
@@ -90,7 +91,9 @@ Commands:
90
91
  activity List submission activity.
91
92
  create Create new submission from file(s).
92
93
  download-document Download document by document URL.
94
+ events List submission events.
93
95
  get Get submission detail.
96
+ get-output Fetch or generate an output from a previous...
94
97
  list-submission-statuses List submission statuses.
95
98
  list-teams List teams.
96
99
 
@@ -112,11 +115,14 @@ Options:
112
115
  --help Show this message and exit.
113
116
 
114
117
  Commands:
115
- activity List submission activity.
116
- check-progress Check the progress of a submission.
117
- fix Extract insurance information from file(s).
118
- get-output Fetch or generate an output from a previous extraction.
118
+ activity List submission activity.
119
+ check-progress Check the progress of a submission.
120
+ fix Extract insurance information from file(s).
121
+ get-output Fetch or generate an output from a previous...
122
+ history List submission activity.
123
+ list-output-formats List available output formats.
119
124
  serverinfo
125
+ update Update an existing SOV with new location data.
120
126
 
121
127
  ```
122
128
 
@@ -140,8 +146,11 @@ Options:
140
146
  --help Show this message and exit.
141
147
 
142
148
  Commands:
143
- bulk-enhance Request data about multiple addresses using async API.
144
- enhance Request data synchronously about a single address.
149
+ bulk-enhance Request data about multiple addresses using async API.
150
+ enhance Request data synchronously about a single address.
151
+ get-datasource Get the configuration for a single datasource by its...
152
+ list-datasources List the datasource configurations the authenticated...
153
+ usage Get API credit usage over a time range, broken down...
145
154
 
146
155
  ```
147
156
 
@@ -160,10 +169,6 @@ Options:
160
169
  PINGDATA_AUTH_TOKEN environment variable.
161
170
  -v, --verbose Can be used multiple times. -v for INFO, -vv
162
171
  for DEBUG, -vvv for very DEBUG.
163
- -D, --delegate-to ORG_SHORT_NAME
164
- Delegate to another organization. Provide
165
- the 'short name' of the desired delegatee.
166
- Requires the `delegate` permission.
167
172
  --help Show this message and exit.
168
173
 
169
174
  Commands:
@@ -74,7 +74,9 @@ Commands:
74
74
  activity List submission activity.
75
75
  create Create new submission from file(s).
76
76
  download-document Download document by document URL.
77
+ events List submission events.
77
78
  get Get submission detail.
79
+ get-output Fetch or generate an output from a previous...
78
80
  list-submission-statuses List submission statuses.
79
81
  list-teams List teams.
80
82
 
@@ -96,11 +98,14 @@ Options:
96
98
  --help Show this message and exit.
97
99
 
98
100
  Commands:
99
- activity List submission activity.
100
- check-progress Check the progress of a submission.
101
- fix Extract insurance information from file(s).
102
- get-output Fetch or generate an output from a previous extraction.
101
+ activity List submission activity.
102
+ check-progress Check the progress of a submission.
103
+ fix Extract insurance information from file(s).
104
+ get-output Fetch or generate an output from a previous...
105
+ history List submission activity.
106
+ list-output-formats List available output formats.
103
107
  serverinfo
108
+ update Update an existing SOV with new location data.
104
109
 
105
110
  ```
106
111
 
@@ -124,8 +129,11 @@ Options:
124
129
  --help Show this message and exit.
125
130
 
126
131
  Commands:
127
- bulk-enhance Request data about multiple addresses using async API.
128
- enhance Request data synchronously about a single address.
132
+ bulk-enhance Request data about multiple addresses using async API.
133
+ enhance Request data synchronously about a single address.
134
+ get-datasource Get the configuration for a single datasource by its...
135
+ list-datasources List the datasource configurations the authenticated...
136
+ usage Get API credit usage over a time range, broken down...
129
137
 
130
138
  ```
131
139
 
@@ -144,10 +152,6 @@ Options:
144
152
  PINGDATA_AUTH_TOKEN environment variable.
145
153
  -v, --verbose Can be used multiple times. -v for INFO, -vv
146
154
  for DEBUG, -vvv for very DEBUG.
147
- -D, --delegate-to ORG_SHORT_NAME
148
- Delegate to another organization. Provide
149
- the 'short name' of the desired delegatee.
150
- Requires the `delegate` permission.
151
155
  --help Show this message and exit.
152
156
 
153
157
  Commands:
@@ -0,0 +1,3 @@
1
+ requests
2
+ click
3
+ typing_extensions>=4.13
@@ -0,0 +1 @@
1
+ __version__ = "1.4.11"
@@ -340,6 +340,53 @@ class PingDataAPIClient(APIClientBase):
340
340
  raise_for_status(response)
341
341
  return response.json()
342
342
 
343
+ def list_datasources(
344
+ self,
345
+ *,
346
+ delegate_to: str | None = None,
347
+ ) -> t.ListDatasourcesResponse:
348
+ """
349
+ List the datasource configurations the authenticated user has access to.
350
+
351
+ :param delegate_to: Optional delegate to use for the request.
352
+
353
+ :return: Dict with a 'configs' key mapping each source code (e.g. 'PG', 'PH') to its
354
+ datasource configuration (required/optional attributes, supported regions,
355
+ output fields, etc.).
356
+ """
357
+ url = self.api_url + "/api/v1/datasources"
358
+ params: dict = {}
359
+ if delegate_to:
360
+ params["delegate_to"] = delegate_to
361
+
362
+ response = self.get(url, params=params)
363
+ raise_for_status(response)
364
+ return response.json()
365
+
366
+ def get_datasource(
367
+ self,
368
+ *,
369
+ code: str,
370
+ delegate_to: str | None = None,
371
+ ) -> t.GetDatasourceResponse:
372
+ """
373
+ Get the configuration for a single datasource by its source code.
374
+
375
+ :param code: Datasource source code (e.g. 'PG', 'PH', 'DTC'). Case-insensitive.
376
+ :param delegate_to: Optional delegate to use for the request.
377
+
378
+ :return: Dict with a 'config' key containing the datasource configuration
379
+ (required/optional attributes, supported regions, output fields, etc.).
380
+ """
381
+ url = self.api_url + f"/api/v1/datasources/{code}"
382
+ params: dict = {}
383
+ if delegate_to:
384
+ params["delegate_to"] = delegate_to
385
+
386
+ response = self.get(url, params=params)
387
+ raise_for_status(response)
388
+ return response.json()
389
+
343
390
  def fetch_bulk_enhance_output(self, request_id: str, filename: str, output_path: str | None = None) -> bytes:
344
391
  """
345
392
  Download a result file from a completed bulk enhance job.
@@ -1,5 +1,6 @@
1
1
  # Copyright 2021-2024 Ping Data Intelligence
2
- from typing import Any, Dict, List, NotRequired, Optional, TypedDict, Literal
2
+ from typing import Any, Dict, List, Optional, Literal
3
+ from typing_extensions import NotRequired, TypedDict
3
4
  from datetime import datetime
4
5
  import enum
5
6
  import functools
@@ -166,9 +167,16 @@ class BatchLocation(SingleLocation):
166
167
  datasource_config: NotRequired[dict | None]
167
168
 
168
169
 
170
+ class EnhanceResponseItem(TypedDict, extra_items=Any):
171
+ is_success: bool
172
+ status_code: int
173
+ fetch_time: float
174
+ error_message: NotRequired[str | None]
175
+
176
+
169
177
  class EnhanceResponse(TypedDict):
170
178
  id: str
171
- location_data: dict[SOURCES, dict]
179
+ location_data: dict[SOURCES, EnhanceResponseItem]
172
180
 
173
181
 
174
182
  class BulkEnhanceResponseCheckProgressResultOutputFile(TypedDict):
@@ -235,3 +243,35 @@ class UsageResponse(TypedDict):
235
243
  end: str
236
244
  buckets: list[UsageBucket]
237
245
  totals: dict[str, int]
246
+
247
+
248
+ class DatasourceOutputField(TypedDict, extra_items=Any):
249
+ type: str
250
+ description: NotRequired[str]
251
+ display_type: NotRequired[str]
252
+ return_values: NotRequired[Any]
253
+ category: NotRequired[list[str]]
254
+
255
+
256
+ class DatasourceConfig(TypedDict):
257
+ source_name: str
258
+ source_code: str
259
+ required_attrs: list[str]
260
+ optional_attrs: list[str]
261
+ requires_credentials: bool
262
+ non_supported_input_values: dict[str, list[str]] | None
263
+ supported_countries: list[str] | None
264
+ supported_us_states: list[str] | None
265
+ excluded_us_states: list[str] | None
266
+ geocode_confidence_required: int | None
267
+ geocode_precision_required: int | None
268
+ output_fields: dict[str, DatasourceOutputField]
269
+ base_output_fields: dict[str, DatasourceOutputField]
270
+
271
+
272
+ class ListDatasourcesResponse(TypedDict):
273
+ configs: dict[str, DatasourceConfig]
274
+
275
+
276
+ class GetDatasourceResponse(TypedDict):
277
+ config: DatasourceConfig
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env python
2
+
3
+ # Copyright 2021-2024 Ping Data Intelligence
4
+
5
+ import json
6
+ import logging
7
+ import pprint
8
+ import time
9
+ from timeit import default_timer as timer
10
+ import click
11
+
12
+ from pingintel_api import PingDataAPIClient
13
+
14
+ from pingintel_api.api_client_base import AuthTokenNotFound
15
+ from pingintel_api.pingdata.types import Location
16
+ from pingintel_api.utils import set_verbosity
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ global start_time
22
+ start_time = None
23
+
24
+
25
+ """
26
+ pingdataapi.py
27
+
28
+ Example Python commandline script for using the Ping Data Technologies Data API to enhance locations with additional data.
29
+ """
30
+
31
+
32
+ @click.group()
33
+ @click.option(
34
+ "-e",
35
+ "--environment",
36
+ type=click.Choice(
37
+ [
38
+ "prod",
39
+ "prodeu",
40
+ "staging",
41
+ "dev",
42
+ ],
43
+ case_sensitive=False,
44
+ ),
45
+ )
46
+ @click.option(
47
+ "-u",
48
+ "--api-url",
49
+ help="Provide base url (instead of environment, primarily for debugging)",
50
+ )
51
+ @click.option(
52
+ "--auth-token",
53
+ help="Provide auth token via --auth-token or PINGDATA_AUTH_TOKEN environment variable.",
54
+ )
55
+ @click.option(
56
+ "-v", "--verbose", count=True, help="Can be used multiple times. -v for INFO, -vv for DEBUG, -vvv for very DEBUG."
57
+ )
58
+ @click.option(
59
+ "-D",
60
+ "--delegate-to",
61
+ metavar="ORG_SHORT_NAME",
62
+ help="Delegate to another organization. Provide the 'short name' of the desired delegatee. Requires the `delegate` permission.",
63
+ )
64
+ @click.pass_context
65
+ def cli(ctx, environment, api_url, auth_token, verbose, delegate_to):
66
+ ctx.ensure_object(dict)
67
+ ctx.obj["environment"] = environment
68
+ ctx.obj["auth_token"] = auth_token
69
+ ctx.obj["api_url"] = api_url
70
+ ctx.obj["delegate_to"] = delegate_to
71
+ set_verbosity(verbose)
72
+
73
+
74
+ def get_client(ctx) -> PingDataAPIClient:
75
+ environment = ctx.obj["environment"]
76
+ auth_token = ctx.obj["auth_token"]
77
+ api_url = ctx.obj["api_url"]
78
+ try:
79
+ client = PingDataAPIClient(environment=environment, auth_token=auth_token, api_url=api_url)
80
+ except AuthTokenNotFound as e:
81
+ click.echo(e)
82
+ raise click.Abort()
83
+
84
+ return client
85
+
86
+
87
+ def parse_extra_kwargs(extra: tuple[str, ...]) -> dict:
88
+ """Parse key=value pairs into a dictionary, converting numeric values."""
89
+ result = {}
90
+ for item in extra:
91
+ if "=" not in item:
92
+ raise click.BadParameter(f"Invalid format '{item}'. Expected key=value")
93
+ key, value = item.split("=", 1)
94
+ # Try to convert to int, then float, otherwise keep as string
95
+ try:
96
+ result[key] = int(value)
97
+ except ValueError:
98
+ try:
99
+ result[key] = float(value)
100
+ except ValueError:
101
+ result[key] = value
102
+ return result
103
+
104
+
105
+ @cli.command()
106
+ @click.pass_context
107
+ @click.option("-a", "--address", multiple=True)
108
+ @click.option(
109
+ "-s",
110
+ "--sources",
111
+ multiple=True,
112
+ required=True,
113
+ )
114
+ @click.option("-c", "--country", type=str, default=None, help="Optional. Provides a country hint to the geocoders.")
115
+ @click.option("--latitude", type=float, default=None, help="Optional. Specify latitude.")
116
+ @click.option("--longitude", type=float, default=None, help="Optional. Specify longitude.")
117
+ @click.option("--timeout", type=float, default=None, help="Optional. Maximum time to wait for response in seconds.")
118
+ @click.option("-r", "--include-raw-response", is_flag=True, help="Optional. Include raw response from all sources.")
119
+ @click.option("--nocache", is_flag=True, help="If set, do not use cache.")
120
+ @click.option(
121
+ "-x",
122
+ "--extra",
123
+ multiple=True,
124
+ metavar="KEY=VALUE",
125
+ help="Extra location kwargs (e.g., -x const__code_rms=RC -x const__num_stories=3). Can be used multiple times.",
126
+ )
127
+ def enhance(
128
+ ctx: click.Context,
129
+ address: str,
130
+ sources: list[str],
131
+ country: str | None,
132
+ latitude: float | None,
133
+ longitude: float | None,
134
+ timeout: float | None = None,
135
+ include_raw_response: bool = False,
136
+ nocache: bool = False,
137
+ extra: tuple[str, ...] = (),
138
+ ):
139
+ """Request data synchronously about a single address."""
140
+
141
+ client = get_client(ctx)
142
+
143
+ extra_location_kwargs = parse_extra_kwargs(extra)
144
+
145
+ response_data = client.enhance(
146
+ address=address,
147
+ country=country,
148
+ latitude=latitude,
149
+ longitude=longitude,
150
+ sources=sources,
151
+ timeout=timeout,
152
+ include_raw_response=include_raw_response,
153
+ nocache=nocache,
154
+ delegate_to=ctx.obj["delegate_to"],
155
+ **extra_location_kwargs,
156
+ )
157
+ click.echo(f"+ Finished querying with result:\n{pprint.pformat(response_data)}")
158
+
159
+
160
+ @cli.command()
161
+ @click.pass_context
162
+ @click.option("-a", "--address", multiple=True)
163
+ @click.option("-f", "--file", type=click.File("r"), help="File containing location data, one per line.")
164
+ @click.option(
165
+ "-s",
166
+ "--sources",
167
+ multiple=True,
168
+ # required=True,
169
+ )
170
+ @click.option("--timeout", type=float, default=None, help="Optional. Maximum time to wait for response in seconds.")
171
+ @click.option("-r", "--include-raw-response", is_flag=True, help="Optional. Include raw response from all sources.")
172
+ @click.option("--nocache", is_flag=True, help="If set, do not use cache.")
173
+ @click.option("--fetch-outputs/--no-fetch-outputs", is_flag=True, default=True)
174
+ @click.option("-v", "--verbose", help="Enable verbose output. Can be used up to 3 times.", count=True)
175
+ def bulk_enhance(
176
+ ctx: click.Context,
177
+ address: list[str],
178
+ file: click.File,
179
+ sources: list[str],
180
+ timeout: float | None,
181
+ include_raw_response: bool,
182
+ nocache: bool,
183
+ fetch_outputs: bool,
184
+ verbose: int,
185
+ ):
186
+ """Request data about multiple addresses using async API."""
187
+ client = get_client(ctx)
188
+
189
+ locations = []
190
+ address_id_ctr = 0
191
+ if address:
192
+ for addr in address:
193
+ address_id_ctr += 1
194
+ locations.append(Location(address=addr, id=f"id_{address_id_ctr:03d}"))
195
+
196
+ if file:
197
+ for line in file:
198
+ address_id_ctr += 1
199
+ locations.append(Location(address=line.strip(), id=f"id_{address_id_ctr:03d}"))
200
+
201
+ response_data = client.bulk_enhance(
202
+ locations=locations,
203
+ sources=sources,
204
+ timeout=timeout,
205
+ include_raw_response=include_raw_response,
206
+ nocache=nocache,
207
+ fetch_outputs=fetch_outputs,
208
+ verbose=verbose,
209
+ delegate_to=ctx.obj["delegate_to"],
210
+ )
211
+ click.echo(f"+ Finished querying with result:\n{pprint.pformat(response_data)}")
212
+
213
+
214
+ @cli.command()
215
+ @click.pass_context
216
+ @click.option(
217
+ "--start",
218
+ type=str,
219
+ default=None,
220
+ help="Start of the time range. Relative offset (-5m, -1h, -30d) or ISO 8601 UTC timestamp. Defaults to -30d.",
221
+ )
222
+ @click.option(
223
+ "--end",
224
+ type=str,
225
+ default=None,
226
+ help="End of the time range. Relative offset or ISO 8601 UTC timestamp. Defaults to now.",
227
+ )
228
+ @click.option(
229
+ "--username",
230
+ type=str,
231
+ default=None,
232
+ help="Filter by username. Defaults to the requesting user. Staff only when querying another user.",
233
+ )
234
+ @click.option(
235
+ "--org-short-name",
236
+ type=str,
237
+ default=None,
238
+ help="Filter by organization short name (e.g. 'AMWNS'). Staff only. Mutually exclusive with --username.",
239
+ )
240
+ def usage(
241
+ ctx: click.Context,
242
+ start: str | None,
243
+ end: str | None,
244
+ username: str | None,
245
+ org_short_name: str | None,
246
+ ):
247
+ """Get API credit usage over a time range, broken down by data source and time bucket."""
248
+ client = get_client(ctx)
249
+ response_data = client.get_usage(
250
+ start=start,
251
+ end=end,
252
+ username=username,
253
+ org_short_name=org_short_name,
254
+ delegate_to=ctx.obj["delegate_to"],
255
+ )
256
+ click.echo(pprint.pformat(response_data))
257
+
258
+
259
+ @cli.command()
260
+ @click.pass_context
261
+ def list_datasources(ctx: click.Context):
262
+ """List the datasource configurations the authenticated user has access to."""
263
+ client = get_client(ctx)
264
+ response_data = client.list_datasources(delegate_to=ctx.obj["delegate_to"])
265
+ click.echo(pprint.pformat(response_data))
266
+
267
+
268
+ def _field_description(info: dict) -> str:
269
+ """Extract the human-readable description from an output-field entry.
270
+
271
+ Field metadata (description, display_type, etc.) is spread flat alongside `type`.
272
+ """
273
+ return str((info or {}).get("description", "") or "")
274
+
275
+
276
+ def _render_datasource_markdown(config: dict) -> str:
277
+ """Render a single datasource config dict as human-readable Markdown."""
278
+ lines: list[str] = []
279
+
280
+ name = config.get("source_name") or config.get("source_code") or "Datasource"
281
+ code = config.get("source_code") or ""
282
+ lines.append(f"# {name} ({code})" if code else f"# {name}")
283
+ lines.append("")
284
+
285
+ conf = config.get("geocode_confidence_required")
286
+ prec = config.get("geocode_precision_required")
287
+ lines.append(f"- **Source code:** {code or 'None'}")
288
+ lines.append(f"- **Requires credentials:** {'Yes' if config.get('requires_credentials') else 'No'}")
289
+ lines.append(f"- **Geocode confidence required:** {conf if conf is not None else 'None'}")
290
+ lines.append(f"- **Geocode precision required:** {prec if prec is not None else 'None'}")
291
+ lines.append("")
292
+
293
+ required = config.get("required_attrs") or []
294
+ optional = config.get("optional_attrs") or []
295
+ lines.append("## Input attributes")
296
+ lines.append("")
297
+ lines.append(f"- **Required:** {', '.join(required) if required else 'None'}")
298
+ lines.append(f"- **Optional:** {', '.join(optional) if optional else 'None'}")
299
+ lines.append("")
300
+
301
+ countries = config.get("supported_countries")
302
+ states = config.get("supported_us_states")
303
+ excluded = config.get("excluded_us_states")
304
+ if countries or states or excluded:
305
+ lines.append("## Supported regions")
306
+ lines.append("")
307
+ if countries:
308
+ lines.append(f"- **Countries:** {', '.join(countries)}")
309
+ if states:
310
+ lines.append(f"- **US states:** {', '.join(states)}")
311
+ if excluded:
312
+ lines.append(f"- **Excluded US states:** {', '.join(excluded)}")
313
+ lines.append("")
314
+
315
+ non_supported = config.get("non_supported_input_values")
316
+ if non_supported:
317
+ lines.append("## Non-supported input values")
318
+ lines.append("")
319
+ for field, values in non_supported.items():
320
+ lines.append(f"- **{field}:** {', '.join(values) if values else 'None'}")
321
+ lines.append("")
322
+
323
+ def _append_field_table(title: str, fields: dict) -> None:
324
+ if not fields:
325
+ return
326
+ lines.append(f"## {title}")
327
+ lines.append("")
328
+ lines.append("| Field | Type | Description |")
329
+ lines.append("| --- | --- | --- |")
330
+ for field_name, info in fields.items():
331
+ field_type = str((info or {}).get("type", "")).replace("|", "\\|")
332
+ description = _field_description(info).replace("|", "\\|").replace("\n", " ")
333
+ lines.append(f"| {field_name} | {field_type} | {description} |")
334
+ # Nested object fields declare their sub-attributes under `fields`.
335
+ for sub_name, sub_info in ((info or {}).get("fields") or {}).items():
336
+ sub_type = str((sub_info or {}).get("type", "")).replace("|", "\\|")
337
+ sub_desc = _field_description(sub_info).replace("|", "\\|").replace("\n", " ")
338
+ lines.append(f"| {field_name}.{sub_name} | {sub_type} | {sub_desc} |")
339
+ lines.append("")
340
+
341
+ _append_field_table("Output fields", config.get("output_fields") or {})
342
+ _append_field_table("Base output fields", config.get("base_output_fields") or {})
343
+
344
+ return "\n".join(lines).rstrip() + "\n"
345
+
346
+
347
+ def _render_datasource_pretty(config: dict) -> str:
348
+ """Render a single datasource config dict as plain readable text (no Markdown tables)."""
349
+ lines: list[str] = []
350
+
351
+ name = config.get("source_name") or config.get("source_code") or "Datasource"
352
+ code = config.get("source_code") or ""
353
+ heading = f"{name} ({code})" if code else name
354
+ lines.append(heading)
355
+ lines.append("=" * len(heading))
356
+ lines.append("")
357
+
358
+ conf = config.get("geocode_confidence_required")
359
+ prec = config.get("geocode_precision_required")
360
+ lines.append(f"Source code: {code or 'None'}")
361
+ lines.append(f"Requires credentials: {'Yes' if config.get('requires_credentials') else 'No'}")
362
+ lines.append(f"Geocode confidence required: {conf if conf is not None else 'None'}")
363
+ lines.append(f"Geocode precision required: {prec if prec is not None else 'None'}")
364
+ lines.append("")
365
+
366
+ required = config.get("required_attrs") or []
367
+ optional = config.get("optional_attrs") or []
368
+ lines.append("Input attributes:")
369
+ lines.append(f" Required: {', '.join(required) if required else 'None'}")
370
+ lines.append(f" Optional: {', '.join(optional) if optional else 'None'}")
371
+ lines.append("")
372
+
373
+ countries = config.get("supported_countries")
374
+ states = config.get("supported_us_states")
375
+ excluded = config.get("excluded_us_states")
376
+ if countries or states or excluded:
377
+ lines.append("Supported regions:")
378
+ if countries:
379
+ lines.append(f" Countries: {', '.join(countries)}")
380
+ if states:
381
+ lines.append(f" US states: {', '.join(states)}")
382
+ if excluded:
383
+ lines.append(f" Excluded US states: {', '.join(excluded)}")
384
+ lines.append("")
385
+
386
+ non_supported = config.get("non_supported_input_values")
387
+ if non_supported:
388
+ lines.append("Non-supported input values:")
389
+ for field, values in non_supported.items():
390
+ lines.append(f" {field}: {', '.join(values) if values else 'None'}")
391
+ lines.append("")
392
+
393
+ def _append_field_section(title: str, fields: dict) -> None:
394
+ if not fields:
395
+ return
396
+ lines.append(f"{title}:")
397
+ for field_name, info in fields.items():
398
+ field_type = str((info or {}).get("type", ""))
399
+ lines.append(f" {field_name} ({field_type})" if field_type else f" {field_name}")
400
+ description = _field_description(info).strip()
401
+ for desc_line in description.splitlines():
402
+ lines.append(f" {desc_line}")
403
+ # Nested object fields declare their sub-attributes under `fields`.
404
+ for sub_name, sub_info in ((info or {}).get("fields") or {}).items():
405
+ sub_type = str((sub_info or {}).get("type", ""))
406
+ dotted = f"{field_name}.{sub_name}"
407
+ lines.append(f" {dotted} ({sub_type})" if sub_type else f" {dotted}")
408
+ for desc_line in _field_description(sub_info).strip().splitlines():
409
+ lines.append(f" {desc_line}")
410
+ lines.append("")
411
+
412
+ _append_field_section("Output fields", config.get("output_fields") or {})
413
+ _append_field_section("Base output fields", config.get("base_output_fields") or {})
414
+
415
+ return "\n".join(lines).rstrip() + "\n"
416
+
417
+
418
+ @cli.command()
419
+ @click.pass_context
420
+ @click.argument("code", type=str)
421
+ @click.option(
422
+ "--format",
423
+ "output_format",
424
+ type=click.Choice(["json", "markdown", "pretty"], case_sensitive=False),
425
+ default="markdown",
426
+ show_default=True,
427
+ help="Output format for the datasource config.",
428
+ )
429
+ def get_datasource(ctx: click.Context, code: str, output_format: str):
430
+ """Get the configuration for a single datasource by its source code (e.g. PG, PH, DTC)."""
431
+ client = get_client(ctx)
432
+ response_data = client.get_datasource(code=code, delegate_to=ctx.obj["delegate_to"])
433
+ output_format = output_format.lower()
434
+ if output_format == "json":
435
+ click.echo(json.dumps(response_data, indent=2))
436
+ return
437
+ config = response_data.get("config", response_data)
438
+ if output_format == "pretty":
439
+ click.echo(_render_datasource_pretty(config))
440
+ else:
441
+ click.echo(_render_datasource_markdown(config))
442
+
443
+
444
+ def main():
445
+ cli()
446
+
447
+
448
+ if __name__ == "__main__":
449
+ main()
@@ -373,3 +373,88 @@ class PingVisionAPIClient(APIClientBase):
373
373
  url = self.api_url + f"/api/v1/submission/{pingid}/add_data_items"
374
374
  response = self.post(url, json={"items": items, "action": action})
375
375
  raise_for_status(response)
376
+
377
+ def export_modeling_options(self, pingid: str):
378
+ url = self.api_url + f"/api/v1/submission/{pingid}/cat/modeling-options/export"
379
+
380
+ response = self.get(url)
381
+
382
+ raise_for_status(response)
383
+
384
+ response_data = response.json()
385
+ return response_data
386
+
387
+ def generate_acc_loc_files_async_start(
388
+ self,
389
+ pingid: str,
390
+ modeling_option_uuids: list[str] = None,
391
+ cat_model_type: str = None,
392
+ use_secondary_modifiers: bool = None,
393
+ use_ping_geocoding: bool = None,
394
+ layer_output: str = None,
395
+ ):
396
+ url = self.api_url + f"/api/v1/submission/{pingid}/cat/acc-loc-files"
397
+
398
+ data = {}
399
+ if modeling_option_uuids:
400
+ data["modeling_option_uuids"] = modeling_option_uuids
401
+ if cat_model_type:
402
+ data["cat_model_type"] = cat_model_type
403
+ if use_secondary_modifiers:
404
+ data["use_secondary_modifiers"] = use_secondary_modifiers
405
+ if use_ping_geocoding:
406
+ data["use_ping_geocoding"] = use_ping_geocoding
407
+ if layer_output:
408
+ data["layer_output"] = layer_output
409
+
410
+ response = self.post(url, json=data)
411
+ if 200 <= response.status_code < 300:
412
+ pass
413
+ # pprint.pprint(response.json())
414
+ else:
415
+ self.logger.warning(f"Error starting Generate Acc/Loc file request:\n{pprint.pformat(response.text)}")
416
+
417
+ raise_for_status(response)
418
+
419
+ response_data = response.json()
420
+ return response_data
421
+
422
+ def generate_acc_loc_files_async_check_progress(self, pingid, uuid):
423
+ status_url = self.api_url + f"/api/v1/submission/{pingid}/cat/acc-loc-files/{uuid}"
424
+
425
+ response = self.get(status_url)
426
+ # pprint.pprint(response.json())
427
+ raise_for_status(response)
428
+
429
+ response_data = response.json()
430
+ return response_data
431
+
432
+ def generate_acc_loc_files(
433
+ self,
434
+ pingid: str,
435
+ modeling_option_uuids: list[str] = None,
436
+ cat_model_type: str = None,
437
+ use_secondary_modifiers: bool = None,
438
+ use_ping_geocoding: bool = None,
439
+ layer_output: str = None,
440
+ ):
441
+ client = self
442
+ response = client.generate_acc_loc_files_async_start(pingid, modeling_option_uuids, cat_model_type, use_secondary_modifiers, use_ping_geocoding, layer_output)
443
+ uuid = response.get("uuid")
444
+ assert uuid
445
+
446
+ response_data = None
447
+ while 1:
448
+ response_data = client.generate_acc_loc_files_async_check_progress(pingid, uuid)
449
+ status = response_data.get("status")
450
+ progress = response_data.get("processing_pct_complete")
451
+ POLL_SECS = 2.5
452
+ if status == "N" or status == "I":
453
+ self.logger.info(f" - Still in progress: {status} ({progress}%)")
454
+ time.sleep(POLL_SECS)
455
+ else:
456
+ break
457
+
458
+ return response_data
459
+
460
+
@@ -315,6 +315,62 @@ def list_submission_statuses(ctx, division):
315
315
  return
316
316
  pprint.pprint(results)
317
317
 
318
+ @cli.command()
319
+ @click.pass_context
320
+ @click.argument("pingid", type=str)
321
+ def modeling_options(ctx, pingid):
322
+ """Export modeling options for a submission."""
323
+ client = get_client(ctx)
324
+
325
+ ret = client.export_modeling_options(pingid=pingid)
326
+ pprint.pprint(ret)
327
+
328
+ @cli.command()
329
+ @click.pass_context
330
+ @click.argument("pingid", type=str)
331
+ @click.option("-m", "--modeling-option-uuids", type=str, multiple=True, help="Generate files for specific modeling options (all if not provided)")
332
+ @click.option("--cat-model-type",type=click.Choice(
333
+ [
334
+ "air",
335
+ "rms",
336
+ ],
337
+ case_sensitive=False,
338
+ ), help="Cat models to generate files for (all if not provided)")
339
+ @click.option("--use-secondary-modifiers",type=bool)
340
+ @click.option("--use-ping-geocoding",type=bool)
341
+ @click.option("--layer-output",type=click.Choice(
342
+ [
343
+ "C",
344
+ "P",
345
+ ],
346
+ case_sensitive=False,
347
+ ),help="C (Combined) or P (Per Layer)",)
348
+ @click.option(
349
+ "--write/--no-write",
350
+ is_flag=True,
351
+ default=True,
352
+ help="(default) Download and write the acc/loc files to disk.",
353
+ )
354
+ def generate_acc_loc_files(ctx, pingid,modeling_option_uuids,cat_model_type,use_secondary_modifiers,use_ping_geocoding,layer_output, write):
355
+ """List submission activity."""
356
+ client = get_client(ctx)
357
+ ret = client.generate_acc_loc_files(pingid, modeling_option_uuids,cat_model_type,use_secondary_modifiers,use_ping_geocoding,layer_output)
358
+
359
+ if write:
360
+ if ret.get("status") == "C":
361
+ for modeling_set in ret.get("modeling_sets", {}):
362
+ if modeling_set.get("status") == "C":
363
+ acc_file_url = modeling_set.get("acc_file_url")
364
+ if acc_file_url:
365
+ ctx.invoke(download_document, document_url=acc_file_url, output=None)
366
+ loc_file_url = modeling_set.get("loc_file_url")
367
+ if loc_file_url:
368
+ ctx.invoke(download_document, document_url=loc_file_url, output=None)
369
+
370
+ pprint.pprint(ret)
371
+
372
+
373
+
318
374
 
319
375
  @cli.command()
320
376
  @click.pass_context
@@ -1,2 +0,0 @@
1
- requests
2
- click
@@ -1 +0,0 @@
1
- __version__ = "1.4.10"
@@ -1,263 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- # Copyright 2021-2024 Ping Data Intelligence
4
-
5
- import logging
6
- import pprint
7
- import time
8
- from timeit import default_timer as timer
9
- import click
10
-
11
- from pingintel_api import PingDataAPIClient
12
-
13
- from pingintel_api.api_client_base import AuthTokenNotFound
14
- from pingintel_api.pingdata.types import Location
15
- from pingintel_api.utils import set_verbosity
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- global start_time
21
- start_time = None
22
-
23
-
24
- """
25
- pingdataapi.py
26
-
27
- Example Python commandline script for using the Ping Data Technologies Data API to enhance locations with additional data.
28
- """
29
-
30
-
31
- @click.group()
32
- @click.option(
33
- "-e",
34
- "--environment",
35
- type=click.Choice(
36
- [
37
- "prod",
38
- "prodeu",
39
- "staging",
40
- "dev",
41
- ],
42
- case_sensitive=False,
43
- ),
44
- )
45
- @click.option(
46
- "-u",
47
- "--api-url",
48
- help="Provide base url (instead of environment, primarily for debugging)",
49
- )
50
- @click.option(
51
- "--auth-token",
52
- help="Provide auth token via --auth-token or PINGDATA_AUTH_TOKEN environment variable.",
53
- )
54
- @click.option(
55
- "-v", "--verbose", count=True, help="Can be used multiple times. -v for INFO, -vv for DEBUG, -vvv for very DEBUG."
56
- )
57
- @click.option(
58
- "-D",
59
- "--delegate-to",
60
- metavar="ORG_SHORT_NAME",
61
- help="Delegate to another organization. Provide the 'short name' of the desired delegatee. Requires the `delegate` permission.",
62
- )
63
- @click.pass_context
64
- def cli(ctx, environment, api_url, auth_token, verbose, delegate_to):
65
- ctx.ensure_object(dict)
66
- ctx.obj["environment"] = environment
67
- ctx.obj["auth_token"] = auth_token
68
- ctx.obj["api_url"] = api_url
69
- ctx.obj["delegate_to"] = delegate_to
70
- set_verbosity(verbose)
71
-
72
-
73
- def get_client(ctx) -> PingDataAPIClient:
74
- environment = ctx.obj["environment"]
75
- auth_token = ctx.obj["auth_token"]
76
- api_url = ctx.obj["api_url"]
77
- try:
78
- client = PingDataAPIClient(environment=environment, auth_token=auth_token, api_url=api_url)
79
- except AuthTokenNotFound as e:
80
- click.echo(e)
81
- raise click.Abort()
82
-
83
- return client
84
-
85
-
86
- def parse_extra_kwargs(extra: tuple[str, ...]) -> dict:
87
- """Parse key=value pairs into a dictionary, converting numeric values."""
88
- result = {}
89
- for item in extra:
90
- if "=" not in item:
91
- raise click.BadParameter(f"Invalid format '{item}'. Expected key=value")
92
- key, value = item.split("=", 1)
93
- # Try to convert to int, then float, otherwise keep as string
94
- try:
95
- result[key] = int(value)
96
- except ValueError:
97
- try:
98
- result[key] = float(value)
99
- except ValueError:
100
- result[key] = value
101
- return result
102
-
103
-
104
- @cli.command()
105
- @click.pass_context
106
- @click.option("-a", "--address", multiple=True)
107
- @click.option(
108
- "-s",
109
- "--sources",
110
- multiple=True,
111
- required=True,
112
- )
113
- @click.option("-c", "--country", type=str, default=None, help="Optional. Provides a country hint to the geocoders.")
114
- @click.option("--latitude", type=float, default=None, help="Optional. Specify latitude.")
115
- @click.option("--longitude", type=float, default=None, help="Optional. Specify longitude.")
116
- @click.option("--timeout", type=float, default=None, help="Optional. Maximum time to wait for response in seconds.")
117
- @click.option("-r", "--include-raw-response", is_flag=True, help="Optional. Include raw response from all sources.")
118
- @click.option("--nocache", is_flag=True, help="If set, do not use cache.")
119
- @click.option(
120
- "-x",
121
- "--extra",
122
- multiple=True,
123
- metavar="KEY=VALUE",
124
- help="Extra location kwargs (e.g., -x const__code_rms=RC -x const__num_stories=3). Can be used multiple times.",
125
- )
126
- def enhance(
127
- ctx: click.Context,
128
- address: str,
129
- sources: list[str],
130
- country: str | None,
131
- latitude: float | None,
132
- longitude: float | None,
133
- timeout: float | None = None,
134
- include_raw_response: bool = False,
135
- nocache: bool = False,
136
- extra: tuple[str, ...] = (),
137
- ):
138
- """Request data synchronously about a single address."""
139
-
140
- client = get_client(ctx)
141
-
142
- extra_location_kwargs = parse_extra_kwargs(extra)
143
-
144
- response_data = client.enhance(
145
- address=address,
146
- country=country,
147
- latitude=latitude,
148
- longitude=longitude,
149
- sources=sources,
150
- timeout=timeout,
151
- include_raw_response=include_raw_response,
152
- nocache=nocache,
153
- delegate_to=ctx.obj["delegate_to"],
154
- **extra_location_kwargs,
155
- )
156
- click.echo(f"+ Finished querying with result:\n{pprint.pformat(response_data)}")
157
-
158
-
159
- @cli.command()
160
- @click.pass_context
161
- @click.option("-a", "--address", multiple=True)
162
- @click.option("-f", "--file", type=click.File("r"), help="File containing location data, one per line.")
163
- @click.option(
164
- "-s",
165
- "--sources",
166
- multiple=True,
167
- # required=True,
168
- )
169
- @click.option("--timeout", type=float, default=None, help="Optional. Maximum time to wait for response in seconds.")
170
- @click.option("-r", "--include-raw-response", is_flag=True, help="Optional. Include raw response from all sources.")
171
- @click.option("--nocache", is_flag=True, help="If set, do not use cache.")
172
- @click.option("--fetch-outputs/--no-fetch-outputs", is_flag=True, default=True)
173
- @click.option("-v", "--verbose", help="Enable verbose output. Can be used up to 3 times.", count=True)
174
- def bulk_enhance(
175
- ctx: click.Context,
176
- address: list[str],
177
- file: click.File,
178
- sources: list[str],
179
- timeout: float | None,
180
- include_raw_response: bool,
181
- nocache: bool,
182
- fetch_outputs: bool,
183
- verbose: int,
184
- ):
185
- """Request data about multiple addresses using async API."""
186
- client = get_client(ctx)
187
-
188
- locations = []
189
- address_id_ctr = 0
190
- if address:
191
- for addr in address:
192
- address_id_ctr += 1
193
- locations.append(Location(address=addr, id=f"id_{address_id_ctr:03d}"))
194
-
195
- if file:
196
- for line in file:
197
- address_id_ctr += 1
198
- locations.append(Location(address=line.strip(), id=f"id_{address_id_ctr:03d}"))
199
-
200
- response_data = client.bulk_enhance(
201
- locations=locations,
202
- sources=sources,
203
- timeout=timeout,
204
- include_raw_response=include_raw_response,
205
- nocache=nocache,
206
- fetch_outputs=fetch_outputs,
207
- verbose=verbose,
208
- delegate_to=ctx.obj["delegate_to"],
209
- )
210
- click.echo(f"+ Finished querying with result:\n{pprint.pformat(response_data)}")
211
-
212
-
213
- @cli.command()
214
- @click.pass_context
215
- @click.option(
216
- "--start",
217
- type=str,
218
- default=None,
219
- help="Start of the time range. Relative offset (-5m, -1h, -30d) or ISO 8601 UTC timestamp. Defaults to -30d.",
220
- )
221
- @click.option(
222
- "--end",
223
- type=str,
224
- default=None,
225
- help="End of the time range. Relative offset or ISO 8601 UTC timestamp. Defaults to now.",
226
- )
227
- @click.option(
228
- "--username",
229
- type=str,
230
- default=None,
231
- help="Filter by username. Defaults to the requesting user. Staff only when querying another user.",
232
- )
233
- @click.option(
234
- "--org-short-name",
235
- type=str,
236
- default=None,
237
- help="Filter by organization short name (e.g. 'AMWNS'). Staff only. Mutually exclusive with --username.",
238
- )
239
- def usage(
240
- ctx: click.Context,
241
- start: str | None,
242
- end: str | None,
243
- username: str | None,
244
- org_short_name: str | None,
245
- ):
246
- """Get API credit usage over a time range, broken down by data source and time bucket."""
247
- client = get_client(ctx)
248
- response_data = client.get_usage(
249
- start=start,
250
- end=end,
251
- username=username,
252
- org_short_name=org_short_name,
253
- delegate_to=ctx.obj["delegate_to"],
254
- )
255
- click.echo(pprint.pformat(response_data))
256
-
257
-
258
- def main():
259
- cli()
260
-
261
-
262
- if __name__ == "__main__":
263
- main()
File without changes
File without changes