parseapi 1.4.0__tar.gz → 1.7.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.
- {parseapi-1.4.0 → parseapi-1.7.0}/CHANGELOG.md +8 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/PKG-INFO +51 -3
- {parseapi-1.4.0 → parseapi-1.7.0}/README.md +50 -2
- {parseapi-1.4.0 → parseapi-1.7.0}/api/public.json +324 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/pyproject.toml +1 -1
- {parseapi-1.4.0 → parseapi-1.7.0}/src/parseapi/__init__.py +1 -1
- {parseapi-1.4.0 → parseapi-1.7.0}/src/parseapi/_client.py +75 -21
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_client.py +12 -0
- parseapi-1.7.0/tests/test_tariff.py +92 -0
- parseapi-1.7.0/tests/test_time_complete.py +54 -0
- parseapi-1.7.0/tests/test_time_gaps.py +33 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/.github/workflows/publish.yml +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/.github/workflows/test.yml +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/.gitignore +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/LICENSE +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/scripts/public_api.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/smoke/smoke.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/src/parseapi/py.typed +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_email_enrichment.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_language.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_name_formatting.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_postal_localities.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_public_api.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_stack.py +0 -0
- {parseapi-1.4.0 → parseapi-1.7.0}/tests/test_version.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Add exact Tariff edition/date selection, answering metadata, contextual search lineage and explanatory null reasons. Explicit selections reject unsupported or mismatched server responses.
|
|
6
|
+
|
|
7
|
+
## 1.6.0 - 2026-09-24
|
|
8
|
+
|
|
9
|
+
Adds Time location inputs and explicit ambiguity candidates, filtered timezone discovery, multiple conversion targets, wall-time disambiguation, and standard/seasonal offset detail. Existing Timezone methods and API 2.0.0 selection remain unchanged.
|
|
10
|
+
|
|
3
11
|
## 1.4.0 - 2026-09-24
|
|
4
12
|
|
|
5
13
|
Adds Australian Postal suburb choices while preserving null, empty, and ambiguous results. Existing calls, compact nearby/distance responses, and API contract `2.0.0` remain unchanged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: parseapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Official ParseAPI client for Python. One key, minimal JSON, fast.
|
|
5
5
|
Project-URL: Homepage, https://parseapi.com
|
|
6
6
|
Project-URL: Documentation, https://parseapi.com/docs
|
|
@@ -36,7 +36,7 @@ Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEA
|
|
|
36
36
|
|
|
37
37
|
## API versions
|
|
38
38
|
|
|
39
|
-
Version 1.
|
|
39
|
+
Version 1.7.0 explicitly selects the API contract supported by this SDK. It sends `Parse-Version: 2.0.0` on every lookup so responses match the API contract supported by the package. Your key and the team's saved default stay the same.
|
|
40
40
|
|
|
41
41
|
Upgrade the dependency in staging, review the [release notes](https://parseapi.com/docs/releases), and test the application before deploying the same code and dependency version to production. Commit your dependency lockfile so the tested package travels with your deployment. Future major SDK upgrades can select a newer API contract.
|
|
42
42
|
|
|
@@ -170,7 +170,49 @@ The next call keeps its usual default unless it also supplies `lang`. Existing `
|
|
|
170
170
|
|
|
171
171
|
## Time
|
|
172
172
|
|
|
173
|
-
`time` returns local ISO `at` with its UTC offset and integer Unix seconds in `unix`. The core `offset` preserves exact precision. Optional `deep.offset_seconds` gives the numeric offset, while `deep.offset_minutes` gives whole minutes. Historical offsets and ISO times can include offset seconds. Omitted `at` means now. With `to`, an offsetless `at` is source wall time. Otherwise it is UTC. Include an offset for repeated local times around a clock change. Current time and conversion use pooled requests on every plan. Coordinate clock fields can be null when the timezone is unknown. Existing `timezone` methods remain supported.
|
|
173
|
+
`time` returns local ISO `at` with its UTC offset and integer Unix seconds in `unix`. The core `offset` preserves exact precision. Optional `deep.offset_seconds` gives the numeric offset, while `deep.offset_minutes` gives whole minutes. Historical offsets and ISO times can include offset seconds. Omitted `at` means now. With `to` or `targets`, an offsetless `at` is source wall time. Otherwise it is UTC. Include an offset for repeated local times around a clock change. Current time and conversion use pooled requests on every plan. Coordinate clock fields can be null when the timezone is unknown. Existing `timezone` methods remain supported.
|
|
174
|
+
|
|
175
|
+
For an offsetless `at` with `to` or `targets`, choose how to handle a clock change with `disambiguation`. It applies to named-zone and coordinate Time calls.
|
|
176
|
+
|
|
177
|
+
| Value | Repeated time | Skipped time |
|
|
178
|
+
| --- | --- | --- |
|
|
179
|
+
| `compatible` (default) | Earlier occurrence | Shift forward by the clock change |
|
|
180
|
+
| `earlier` | Earlier occurrence | Shift backward by the clock change |
|
|
181
|
+
| `later` | Later occurrence | Shift forward by the clock change |
|
|
182
|
+
| `reject` | `400 ambiguous_time` | `400 nonexistent_time` |
|
|
183
|
+
|
|
184
|
+
An explicit UTC offset selects an instant directly. For example, `2026-11-01T01:30:00-04:00` and `2026-11-01T01:30:00-05:00` identify the two New York occurrences. A valid `disambiguation` value has no effect on explicit instants, current-time requests or lookups without `to` or `targets`. For user-entered appointment times, start with `reject`. Handle `ambiguous_time` or `nonexistent_time` by collecting an explicit offset or an earlier/later choice from the user. Other malformed input still uses `invalid_request`.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
result = parse.time("America/New_York", at="2026-11-01T01:30:00",
|
|
188
|
+
to="UTC", disambiguation="later")
|
|
189
|
+
print(result["to"]["at"]) # 2026-11-01T06:30:00+00:00
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Canonical Time `deep` includes the pinned rule edition in `deep.timezone_database_version` and source-wall resolution in `deep.resolution`. Resolution records `kind` (`unique`, `overlap` or `gap`), the selected policy, signed `adjustment_seconds`, and chronological alternatives with exact `at`, Unix seconds and UTC offset. Unique times have an empty alternatives list. Explicit instants, current time and lookups without conversion have null resolution. Destination detail stays compact.
|
|
193
|
+
|
|
194
|
+
Search serving IANA IDs by city or region, or omit the query to list all (Go and Rust use an empty string). Discovery returns `timezone_database_version` and sorted `timezones`. No search matches returns `timezones: []`.
|
|
195
|
+
|
|
196
|
+
Pass `targets` to convert one instant to 1-10 zones in a single pooled request. The native list preserves order and duplicates. Use `targets` instead of `to`. The response adds `targets`, with optional detail inside each target. Unknown source coordinates return `targets: null`. An unknown destination rejects the whole request with `not_found`. Omission keeps the original response shape.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
zones = parse.time.zones('New York')
|
|
200
|
+
result = parse.time('UTC', at='2026-09-24T12:00:00Z',
|
|
201
|
+
targets=['America/New_York', 'Asia/Tokyo'])
|
|
202
|
+
print(zones['timezones'], result['targets'])
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Location inputs and timezone filters
|
|
206
|
+
|
|
207
|
+
`parse.time(iata="JFK", deep=True)` and `parse.time.zones(country="US", dst=False, observes_dst=True, details=True)`.
|
|
208
|
+
|
|
209
|
+
Choose one explicit location input: IP, exact city name or stable city ID, country, IATA airport, ICAO airport, port UN/LOCODE, or address. Country and state can narrow a city or address. State requires country. Address lookup requires US country context and a strict address-point match. Port lookup covers the reviewed port subset, not every assigned UN/LOCODE. IP lookup always uses the supplied IP.
|
|
210
|
+
|
|
211
|
+
Location calls add `location` with `status`, `candidates`, `truncated`, `source` and the typed input. Check `status` before using the clock: ambiguous or missing locations retain null time fields. Candidate coordinates and IDs can also be null. A country with multiple timezones does not silently choose one. Named-zone and coordinate calls retain their existing signatures.
|
|
212
|
+
|
|
213
|
+
Timezone discovery accepts country, IANA area, exact signed offset, abbreviation, DST-at-instant and observes-DST-during-year filters. `at` selects the common instant, `sort` selects timezone or offset order, and `details` adds `zones` rows plus the evaluation `at`. The default `timezones` list stays compact. False DST filters are sent explicitly. An abbreviation returns candidate zones rather than choosing one. Observes-DST uses the UTC calendar year containing `at`.
|
|
214
|
+
|
|
215
|
+
Source deep adds `standard_offset`, `standard_offset_seconds`, signed `dst_offset_seconds` and `season`. Seasonal adjustments can be negative. `season` describes the current DST-flag interval, or the next within 400 days, with actual before/after transition facts and signed `change_seconds`. Unknown boundaries remain null. These fields are optional and nullable, and destination deep stays compact.
|
|
174
216
|
|
|
175
217
|
## Measurements
|
|
176
218
|
|
|
@@ -208,6 +250,12 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
|
|
|
208
250
|
|
|
209
251
|
Tariff starts with the general schedule line. Paid deep adds units and the special and other schedule columns. An optional origin then resolves country-specific measures. The three calls below show those successive choices. Without origin, schedule detail is still returned and origin-dependent fields are null. A null effective rate is not a zero rate.
|
|
210
252
|
|
|
253
|
+
Tariff lookup and search accept an optional `edition` fingerprint and `date` (`YYYY-MM-DD`). The edition pins exact immutable source bytes. A date is accepted only when verified source coverage exists. An edition without a date returns undated schedule context (`date: null`). Default requests use today. Paid detail exposes an open-string `reason` when `effective_rate` is null, including `incomplete_coverage`. A null rate never means zero. Explicit selections fail with `tariff_selection_mismatch` if an older server ignores the requested scope.
|
|
254
|
+
|
|
255
|
+
Origin means where the goods originate, not where they ship from. The effective rate covers matched stored schedule measures only. It is not complete duty or landed cost.
|
|
256
|
+
|
|
257
|
+
Codes contain 4, 6, 8 or 10 ASCII digits; dots and whitespace are optional. Search returns up to 20 description matches with parent `lineage` so a result named "Other" has context. Search is not product classification. In deep, `measures: null` means origin-dependent measures were not resolved. `measures: []` means the resolved lookup found none.
|
|
258
|
+
|
|
211
259
|
```python
|
|
212
260
|
parse.tariff("8471.30.01.00")
|
|
213
261
|
parse.tariff("8471.30.01.00", deep=True)
|
|
@@ -17,7 +17,7 @@ Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEA
|
|
|
17
17
|
|
|
18
18
|
## API versions
|
|
19
19
|
|
|
20
|
-
Version 1.
|
|
20
|
+
Version 1.7.0 explicitly selects the API contract supported by this SDK. It sends `Parse-Version: 2.0.0` on every lookup so responses match the API contract supported by the package. Your key and the team's saved default stay the same.
|
|
21
21
|
|
|
22
22
|
Upgrade the dependency in staging, review the [release notes](https://parseapi.com/docs/releases), and test the application before deploying the same code and dependency version to production. Commit your dependency lockfile so the tested package travels with your deployment. Future major SDK upgrades can select a newer API contract.
|
|
23
23
|
|
|
@@ -151,7 +151,49 @@ The next call keeps its usual default unless it also supplies `lang`. Existing `
|
|
|
151
151
|
|
|
152
152
|
## Time
|
|
153
153
|
|
|
154
|
-
`time` returns local ISO `at` with its UTC offset and integer Unix seconds in `unix`. The core `offset` preserves exact precision. Optional `deep.offset_seconds` gives the numeric offset, while `deep.offset_minutes` gives whole minutes. Historical offsets and ISO times can include offset seconds. Omitted `at` means now. With `to`, an offsetless `at` is source wall time. Otherwise it is UTC. Include an offset for repeated local times around a clock change. Current time and conversion use pooled requests on every plan. Coordinate clock fields can be null when the timezone is unknown. Existing `timezone` methods remain supported.
|
|
154
|
+
`time` returns local ISO `at` with its UTC offset and integer Unix seconds in `unix`. The core `offset` preserves exact precision. Optional `deep.offset_seconds` gives the numeric offset, while `deep.offset_minutes` gives whole minutes. Historical offsets and ISO times can include offset seconds. Omitted `at` means now. With `to` or `targets`, an offsetless `at` is source wall time. Otherwise it is UTC. Include an offset for repeated local times around a clock change. Current time and conversion use pooled requests on every plan. Coordinate clock fields can be null when the timezone is unknown. Existing `timezone` methods remain supported.
|
|
155
|
+
|
|
156
|
+
For an offsetless `at` with `to` or `targets`, choose how to handle a clock change with `disambiguation`. It applies to named-zone and coordinate Time calls.
|
|
157
|
+
|
|
158
|
+
| Value | Repeated time | Skipped time |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `compatible` (default) | Earlier occurrence | Shift forward by the clock change |
|
|
161
|
+
| `earlier` | Earlier occurrence | Shift backward by the clock change |
|
|
162
|
+
| `later` | Later occurrence | Shift forward by the clock change |
|
|
163
|
+
| `reject` | `400 ambiguous_time` | `400 nonexistent_time` |
|
|
164
|
+
|
|
165
|
+
An explicit UTC offset selects an instant directly. For example, `2026-11-01T01:30:00-04:00` and `2026-11-01T01:30:00-05:00` identify the two New York occurrences. A valid `disambiguation` value has no effect on explicit instants, current-time requests or lookups without `to` or `targets`. For user-entered appointment times, start with `reject`. Handle `ambiguous_time` or `nonexistent_time` by collecting an explicit offset or an earlier/later choice from the user. Other malformed input still uses `invalid_request`.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
result = parse.time("America/New_York", at="2026-11-01T01:30:00",
|
|
169
|
+
to="UTC", disambiguation="later")
|
|
170
|
+
print(result["to"]["at"]) # 2026-11-01T06:30:00+00:00
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Canonical Time `deep` includes the pinned rule edition in `deep.timezone_database_version` and source-wall resolution in `deep.resolution`. Resolution records `kind` (`unique`, `overlap` or `gap`), the selected policy, signed `adjustment_seconds`, and chronological alternatives with exact `at`, Unix seconds and UTC offset. Unique times have an empty alternatives list. Explicit instants, current time and lookups without conversion have null resolution. Destination detail stays compact.
|
|
174
|
+
|
|
175
|
+
Search serving IANA IDs by city or region, or omit the query to list all (Go and Rust use an empty string). Discovery returns `timezone_database_version` and sorted `timezones`. No search matches returns `timezones: []`.
|
|
176
|
+
|
|
177
|
+
Pass `targets` to convert one instant to 1-10 zones in a single pooled request. The native list preserves order and duplicates. Use `targets` instead of `to`. The response adds `targets`, with optional detail inside each target. Unknown source coordinates return `targets: null`. An unknown destination rejects the whole request with `not_found`. Omission keeps the original response shape.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
zones = parse.time.zones('New York')
|
|
181
|
+
result = parse.time('UTC', at='2026-09-24T12:00:00Z',
|
|
182
|
+
targets=['America/New_York', 'Asia/Tokyo'])
|
|
183
|
+
print(zones['timezones'], result['targets'])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Location inputs and timezone filters
|
|
187
|
+
|
|
188
|
+
`parse.time(iata="JFK", deep=True)` and `parse.time.zones(country="US", dst=False, observes_dst=True, details=True)`.
|
|
189
|
+
|
|
190
|
+
Choose one explicit location input: IP, exact city name or stable city ID, country, IATA airport, ICAO airport, port UN/LOCODE, or address. Country and state can narrow a city or address. State requires country. Address lookup requires US country context and a strict address-point match. Port lookup covers the reviewed port subset, not every assigned UN/LOCODE. IP lookup always uses the supplied IP.
|
|
191
|
+
|
|
192
|
+
Location calls add `location` with `status`, `candidates`, `truncated`, `source` and the typed input. Check `status` before using the clock: ambiguous or missing locations retain null time fields. Candidate coordinates and IDs can also be null. A country with multiple timezones does not silently choose one. Named-zone and coordinate calls retain their existing signatures.
|
|
193
|
+
|
|
194
|
+
Timezone discovery accepts country, IANA area, exact signed offset, abbreviation, DST-at-instant and observes-DST-during-year filters. `at` selects the common instant, `sort` selects timezone or offset order, and `details` adds `zones` rows plus the evaluation `at`. The default `timezones` list stays compact. False DST filters are sent explicitly. An abbreviation returns candidate zones rather than choosing one. Observes-DST uses the UTC calendar year containing `at`.
|
|
195
|
+
|
|
196
|
+
Source deep adds `standard_offset`, `standard_offset_seconds`, signed `dst_offset_seconds` and `season`. Seasonal adjustments can be negative. `season` describes the current DST-flag interval, or the next within 400 days, with actual before/after transition facts and signed `change_seconds`. Unknown boundaries remain null. These fields are optional and nullable, and destination deep stays compact.
|
|
155
197
|
|
|
156
198
|
## Measurements
|
|
157
199
|
|
|
@@ -189,6 +231,12 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
|
|
|
189
231
|
|
|
190
232
|
Tariff starts with the general schedule line. Paid deep adds units and the special and other schedule columns. An optional origin then resolves country-specific measures. The three calls below show those successive choices. Without origin, schedule detail is still returned and origin-dependent fields are null. A null effective rate is not a zero rate.
|
|
191
233
|
|
|
234
|
+
Tariff lookup and search accept an optional `edition` fingerprint and `date` (`YYYY-MM-DD`). The edition pins exact immutable source bytes. A date is accepted only when verified source coverage exists. An edition without a date returns undated schedule context (`date: null`). Default requests use today. Paid detail exposes an open-string `reason` when `effective_rate` is null, including `incomplete_coverage`. A null rate never means zero. Explicit selections fail with `tariff_selection_mismatch` if an older server ignores the requested scope.
|
|
235
|
+
|
|
236
|
+
Origin means where the goods originate, not where they ship from. The effective rate covers matched stored schedule measures only. It is not complete duty or landed cost.
|
|
237
|
+
|
|
238
|
+
Codes contain 4, 6, 8 or 10 ASCII digits; dots and whitespace are optional. Search returns up to 20 description matches with parent `lineage` so a result named "Other" has context. Search is not product classification. In deep, `measures: null` means origin-dependent measures were not resolved. `measures: []` means the resolved lookup found none.
|
|
239
|
+
|
|
192
240
|
```python
|
|
193
241
|
parse.tariff("8471.30.01.00")
|
|
194
242
|
parse.tariff("8471.30.01.00", deep=True)
|
|
@@ -1324,6 +1324,18 @@
|
|
|
1324
1324
|
"kind": "KEYWORD_ONLY",
|
|
1325
1325
|
"name": "origin",
|
|
1326
1326
|
"type": "Optional[str]"
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
"default": null,
|
|
1330
|
+
"kind": "KEYWORD_ONLY",
|
|
1331
|
+
"name": "edition",
|
|
1332
|
+
"type": "Optional[str]"
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
"default": null,
|
|
1336
|
+
"kind": "KEYWORD_ONLY",
|
|
1337
|
+
"name": "date",
|
|
1338
|
+
"type": "Optional[str]"
|
|
1327
1339
|
}
|
|
1328
1340
|
],
|
|
1329
1341
|
"returns": "Json"
|
|
@@ -1335,6 +1347,18 @@
|
|
|
1335
1347
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
1336
1348
|
"name": "query",
|
|
1337
1349
|
"type": "str"
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
"default": null,
|
|
1353
|
+
"kind": "KEYWORD_ONLY",
|
|
1354
|
+
"name": "edition",
|
|
1355
|
+
"type": "Optional[str]"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
"default": null,
|
|
1359
|
+
"kind": "KEYWORD_ONLY",
|
|
1360
|
+
"name": "date",
|
|
1361
|
+
"type": "Optional[str]"
|
|
1338
1362
|
}
|
|
1339
1363
|
],
|
|
1340
1364
|
"returns": "Json"
|
|
@@ -1371,6 +1395,66 @@
|
|
|
1371
1395
|
"kind": "KEYWORD_ONLY",
|
|
1372
1396
|
"name": "lang",
|
|
1373
1397
|
"type": "Optional[str]"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
"default": null,
|
|
1401
|
+
"kind": "KEYWORD_ONLY",
|
|
1402
|
+
"name": "disambiguation",
|
|
1403
|
+
"type": "Optional[str]"
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
"default": null,
|
|
1407
|
+
"kind": "KEYWORD_ONLY",
|
|
1408
|
+
"name": "targets",
|
|
1409
|
+
"type": "Optional[list[str]]"
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
"default": null,
|
|
1413
|
+
"kind": "KEYWORD_ONLY",
|
|
1414
|
+
"name": "ip",
|
|
1415
|
+
"type": "Optional[str]"
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
"default": null,
|
|
1419
|
+
"kind": "KEYWORD_ONLY",
|
|
1420
|
+
"name": "city",
|
|
1421
|
+
"type": "Optional[str]"
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
"default": null,
|
|
1425
|
+
"kind": "KEYWORD_ONLY",
|
|
1426
|
+
"name": "country",
|
|
1427
|
+
"type": "Optional[str]"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
"default": null,
|
|
1431
|
+
"kind": "KEYWORD_ONLY",
|
|
1432
|
+
"name": "state",
|
|
1433
|
+
"type": "Optional[str]"
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
"default": null,
|
|
1437
|
+
"kind": "KEYWORD_ONLY",
|
|
1438
|
+
"name": "iata",
|
|
1439
|
+
"type": "Optional[str]"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"default": null,
|
|
1443
|
+
"kind": "KEYWORD_ONLY",
|
|
1444
|
+
"name": "icao",
|
|
1445
|
+
"type": "Optional[str]"
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
"default": null,
|
|
1449
|
+
"kind": "KEYWORD_ONLY",
|
|
1450
|
+
"name": "unlocode",
|
|
1451
|
+
"type": "Optional[str]"
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
"default": null,
|
|
1455
|
+
"kind": "KEYWORD_ONLY",
|
|
1456
|
+
"name": "address",
|
|
1457
|
+
"type": "Optional[str]"
|
|
1374
1458
|
}
|
|
1375
1459
|
],
|
|
1376
1460
|
"returns": "Json"
|
|
@@ -1411,6 +1495,84 @@
|
|
|
1411
1495
|
"kind": "KEYWORD_ONLY",
|
|
1412
1496
|
"name": "lang",
|
|
1413
1497
|
"type": "Optional[str]"
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
"default": null,
|
|
1501
|
+
"kind": "KEYWORD_ONLY",
|
|
1502
|
+
"name": "disambiguation",
|
|
1503
|
+
"type": "Optional[str]"
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
"default": null,
|
|
1507
|
+
"kind": "KEYWORD_ONLY",
|
|
1508
|
+
"name": "targets",
|
|
1509
|
+
"type": "Optional[list[str]]"
|
|
1510
|
+
}
|
|
1511
|
+
],
|
|
1512
|
+
"returns": "Json"
|
|
1513
|
+
},
|
|
1514
|
+
"time.zones": {
|
|
1515
|
+
"async": true,
|
|
1516
|
+
"parameters": [
|
|
1517
|
+
{
|
|
1518
|
+
"default": null,
|
|
1519
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
1520
|
+
"name": "query",
|
|
1521
|
+
"type": "Optional[str]"
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
"default": null,
|
|
1525
|
+
"kind": "KEYWORD_ONLY",
|
|
1526
|
+
"name": "country",
|
|
1527
|
+
"type": "Optional[str]"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
"default": null,
|
|
1531
|
+
"kind": "KEYWORD_ONLY",
|
|
1532
|
+
"name": "area",
|
|
1533
|
+
"type": "Optional[str]"
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
"default": null,
|
|
1537
|
+
"kind": "KEYWORD_ONLY",
|
|
1538
|
+
"name": "offset",
|
|
1539
|
+
"type": "Optional[str]"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
"default": null,
|
|
1543
|
+
"kind": "KEYWORD_ONLY",
|
|
1544
|
+
"name": "abbreviation",
|
|
1545
|
+
"type": "Optional[str]"
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
"default": null,
|
|
1549
|
+
"kind": "KEYWORD_ONLY",
|
|
1550
|
+
"name": "dst",
|
|
1551
|
+
"type": "Optional[bool]"
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
"default": null,
|
|
1555
|
+
"kind": "KEYWORD_ONLY",
|
|
1556
|
+
"name": "observes_dst",
|
|
1557
|
+
"type": "Optional[bool]"
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
"default": null,
|
|
1561
|
+
"kind": "KEYWORD_ONLY",
|
|
1562
|
+
"name": "at",
|
|
1563
|
+
"type": "Optional[str]"
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
"default": false,
|
|
1567
|
+
"kind": "KEYWORD_ONLY",
|
|
1568
|
+
"name": "details",
|
|
1569
|
+
"type": "bool"
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
"default": null,
|
|
1573
|
+
"kind": "KEYWORD_ONLY",
|
|
1574
|
+
"name": "sort",
|
|
1575
|
+
"type": "Optional[str]"
|
|
1414
1576
|
}
|
|
1415
1577
|
],
|
|
1416
1578
|
"returns": "Json"
|
|
@@ -2901,6 +3063,18 @@
|
|
|
2901
3063
|
"kind": "KEYWORD_ONLY",
|
|
2902
3064
|
"name": "origin",
|
|
2903
3065
|
"type": "Optional[str]"
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
"default": null,
|
|
3069
|
+
"kind": "KEYWORD_ONLY",
|
|
3070
|
+
"name": "edition",
|
|
3071
|
+
"type": "Optional[str]"
|
|
3072
|
+
},
|
|
3073
|
+
{
|
|
3074
|
+
"default": null,
|
|
3075
|
+
"kind": "KEYWORD_ONLY",
|
|
3076
|
+
"name": "date",
|
|
3077
|
+
"type": "Optional[str]"
|
|
2904
3078
|
}
|
|
2905
3079
|
],
|
|
2906
3080
|
"returns": "Json"
|
|
@@ -2912,6 +3086,18 @@
|
|
|
2912
3086
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
2913
3087
|
"name": "query",
|
|
2914
3088
|
"type": "str"
|
|
3089
|
+
},
|
|
3090
|
+
{
|
|
3091
|
+
"default": null,
|
|
3092
|
+
"kind": "KEYWORD_ONLY",
|
|
3093
|
+
"name": "edition",
|
|
3094
|
+
"type": "Optional[str]"
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
"default": null,
|
|
3098
|
+
"kind": "KEYWORD_ONLY",
|
|
3099
|
+
"name": "date",
|
|
3100
|
+
"type": "Optional[str]"
|
|
2915
3101
|
}
|
|
2916
3102
|
],
|
|
2917
3103
|
"returns": "Json"
|
|
@@ -2948,6 +3134,66 @@
|
|
|
2948
3134
|
"kind": "KEYWORD_ONLY",
|
|
2949
3135
|
"name": "lang",
|
|
2950
3136
|
"type": "Optional[str]"
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
"default": null,
|
|
3140
|
+
"kind": "KEYWORD_ONLY",
|
|
3141
|
+
"name": "disambiguation",
|
|
3142
|
+
"type": "Optional[str]"
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
"default": null,
|
|
3146
|
+
"kind": "KEYWORD_ONLY",
|
|
3147
|
+
"name": "targets",
|
|
3148
|
+
"type": "Optional[list[str]]"
|
|
3149
|
+
},
|
|
3150
|
+
{
|
|
3151
|
+
"default": null,
|
|
3152
|
+
"kind": "KEYWORD_ONLY",
|
|
3153
|
+
"name": "ip",
|
|
3154
|
+
"type": "Optional[str]"
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
"default": null,
|
|
3158
|
+
"kind": "KEYWORD_ONLY",
|
|
3159
|
+
"name": "city",
|
|
3160
|
+
"type": "Optional[str]"
|
|
3161
|
+
},
|
|
3162
|
+
{
|
|
3163
|
+
"default": null,
|
|
3164
|
+
"kind": "KEYWORD_ONLY",
|
|
3165
|
+
"name": "country",
|
|
3166
|
+
"type": "Optional[str]"
|
|
3167
|
+
},
|
|
3168
|
+
{
|
|
3169
|
+
"default": null,
|
|
3170
|
+
"kind": "KEYWORD_ONLY",
|
|
3171
|
+
"name": "state",
|
|
3172
|
+
"type": "Optional[str]"
|
|
3173
|
+
},
|
|
3174
|
+
{
|
|
3175
|
+
"default": null,
|
|
3176
|
+
"kind": "KEYWORD_ONLY",
|
|
3177
|
+
"name": "iata",
|
|
3178
|
+
"type": "Optional[str]"
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
"default": null,
|
|
3182
|
+
"kind": "KEYWORD_ONLY",
|
|
3183
|
+
"name": "icao",
|
|
3184
|
+
"type": "Optional[str]"
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
"default": null,
|
|
3188
|
+
"kind": "KEYWORD_ONLY",
|
|
3189
|
+
"name": "unlocode",
|
|
3190
|
+
"type": "Optional[str]"
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
"default": null,
|
|
3194
|
+
"kind": "KEYWORD_ONLY",
|
|
3195
|
+
"name": "address",
|
|
3196
|
+
"type": "Optional[str]"
|
|
2951
3197
|
}
|
|
2952
3198
|
],
|
|
2953
3199
|
"returns": "Json"
|
|
@@ -2988,6 +3234,84 @@
|
|
|
2988
3234
|
"kind": "KEYWORD_ONLY",
|
|
2989
3235
|
"name": "lang",
|
|
2990
3236
|
"type": "Optional[str]"
|
|
3237
|
+
},
|
|
3238
|
+
{
|
|
3239
|
+
"default": null,
|
|
3240
|
+
"kind": "KEYWORD_ONLY",
|
|
3241
|
+
"name": "disambiguation",
|
|
3242
|
+
"type": "Optional[str]"
|
|
3243
|
+
},
|
|
3244
|
+
{
|
|
3245
|
+
"default": null,
|
|
3246
|
+
"kind": "KEYWORD_ONLY",
|
|
3247
|
+
"name": "targets",
|
|
3248
|
+
"type": "Optional[list[str]]"
|
|
3249
|
+
}
|
|
3250
|
+
],
|
|
3251
|
+
"returns": "Json"
|
|
3252
|
+
},
|
|
3253
|
+
"time.zones": {
|
|
3254
|
+
"async": false,
|
|
3255
|
+
"parameters": [
|
|
3256
|
+
{
|
|
3257
|
+
"default": null,
|
|
3258
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
3259
|
+
"name": "query",
|
|
3260
|
+
"type": "Optional[str]"
|
|
3261
|
+
},
|
|
3262
|
+
{
|
|
3263
|
+
"default": null,
|
|
3264
|
+
"kind": "KEYWORD_ONLY",
|
|
3265
|
+
"name": "country",
|
|
3266
|
+
"type": "Optional[str]"
|
|
3267
|
+
},
|
|
3268
|
+
{
|
|
3269
|
+
"default": null,
|
|
3270
|
+
"kind": "KEYWORD_ONLY",
|
|
3271
|
+
"name": "area",
|
|
3272
|
+
"type": "Optional[str]"
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
"default": null,
|
|
3276
|
+
"kind": "KEYWORD_ONLY",
|
|
3277
|
+
"name": "offset",
|
|
3278
|
+
"type": "Optional[str]"
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
"default": null,
|
|
3282
|
+
"kind": "KEYWORD_ONLY",
|
|
3283
|
+
"name": "abbreviation",
|
|
3284
|
+
"type": "Optional[str]"
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
"default": null,
|
|
3288
|
+
"kind": "KEYWORD_ONLY",
|
|
3289
|
+
"name": "dst",
|
|
3290
|
+
"type": "Optional[bool]"
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
"default": null,
|
|
3294
|
+
"kind": "KEYWORD_ONLY",
|
|
3295
|
+
"name": "observes_dst",
|
|
3296
|
+
"type": "Optional[bool]"
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
"default": null,
|
|
3300
|
+
"kind": "KEYWORD_ONLY",
|
|
3301
|
+
"name": "at",
|
|
3302
|
+
"type": "Optional[str]"
|
|
3303
|
+
},
|
|
3304
|
+
{
|
|
3305
|
+
"default": false,
|
|
3306
|
+
"kind": "KEYWORD_ONLY",
|
|
3307
|
+
"name": "details",
|
|
3308
|
+
"type": "bool"
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
"default": null,
|
|
3312
|
+
"kind": "KEYWORD_ONLY",
|
|
3313
|
+
"name": "sort",
|
|
3314
|
+
"type": "Optional[str]"
|
|
2991
3315
|
}
|
|
2992
3316
|
],
|
|
2993
3317
|
"returns": "Json"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import math
|
|
5
6
|
import random
|
|
6
7
|
import time
|
|
@@ -10,7 +11,7 @@ from urllib.parse import quote
|
|
|
10
11
|
|
|
11
12
|
import httpx
|
|
12
13
|
|
|
13
|
-
VERSION = "1.
|
|
14
|
+
VERSION = "1.7.0"
|
|
14
15
|
_API_VERSION = "2.0.0"
|
|
15
16
|
DEFAULT_BASE_URL = "https://api.parseapi.com"
|
|
16
17
|
DEFAULT_TIMEOUT = 10.0
|
|
@@ -474,19 +475,30 @@ class _NaicsSync:
|
|
|
474
475
|
return self._client._get("/naics", {"q": query, "limit": limit, "deep": deep})
|
|
475
476
|
|
|
476
477
|
|
|
478
|
+
def _tariff_selection(result: Json, edition: Optional[str], date: Optional[str]) -> Json:
|
|
479
|
+
if (edition is not None or date is not None) and (
|
|
480
|
+
not isinstance(result.get("edition"), str) or re.fullmatch(r"[a-f0-9]{64}", result["edition"]) is None
|
|
481
|
+
or (edition is not None and result.get("edition") != edition)
|
|
482
|
+
or (result.get("date") != date)
|
|
483
|
+
):
|
|
484
|
+
raise ParseAPIError(0, "tariff_selection_mismatch", "Tariff response did not confirm the requested edition/date. The server may not support this selection.", None, None)
|
|
485
|
+
return result
|
|
486
|
+
|
|
487
|
+
|
|
477
488
|
class _TariffSync:
|
|
478
489
|
def __init__(self, client: ParseAPI):
|
|
479
490
|
self._client = client
|
|
480
491
|
|
|
481
|
-
def __call__(self, code: str, *, deep: bool = False, origin: Optional[str] = None) -> Json:
|
|
492
|
+
def __call__(self, code: str, *, deep: bool = False, origin: Optional[str] = None, edition: Optional[str] = None, date: Optional[str] = None) -> Json:
|
|
482
493
|
"""Look up the general US duty schedule line. Paid deep adds units and the special and other
|
|
483
494
|
schedule columns. Add origin with deep to resolve country-specific measures. Without
|
|
484
495
|
origin, schedule detail remains available and origin-dependent fields are null. A null
|
|
485
496
|
effective rate is not a zero rate."""
|
|
486
|
-
return self._client._get(f"/tariff/{_seg(code)}", {"deep": deep, "origin": origin})
|
|
497
|
+
return _tariff_selection(self._client._get(f"/tariff/{_seg(code)}", {"deep": deep, "origin": origin, "edition": edition, "date": date}), edition, date)
|
|
498
|
+
|
|
499
|
+
def search(self, query: str, *, edition: Optional[str] = None, date: Optional[str] = None) -> Json:
|
|
500
|
+
return _tariff_selection(self._client._get("/tariff", {"q": query, "edition": edition, "date": date}), edition, date)
|
|
487
501
|
|
|
488
|
-
def search(self, query: str) -> Json:
|
|
489
|
-
return self._client._get("/tariff", {"q": query})
|
|
490
502
|
|
|
491
503
|
|
|
492
504
|
class AsyncParseAPI:
|
|
@@ -861,15 +873,16 @@ class _TariffAsync:
|
|
|
861
873
|
def __init__(self, client: AsyncParseAPI):
|
|
862
874
|
self._client = client
|
|
863
875
|
|
|
864
|
-
async def __call__(self, code: str, *, deep: bool = False, origin: Optional[str] = None) -> Json:
|
|
876
|
+
async def __call__(self, code: str, *, deep: bool = False, origin: Optional[str] = None, edition: Optional[str] = None, date: Optional[str] = None) -> Json:
|
|
865
877
|
"""Look up the general US duty schedule line. Paid deep adds units and the special and other
|
|
866
878
|
schedule columns. Add origin with deep to resolve country-specific measures. Without
|
|
867
879
|
origin, schedule detail remains available and origin-dependent fields are null. A null
|
|
868
880
|
effective rate is not a zero rate."""
|
|
869
|
-
return await self._client._get(f"/tariff/{_seg(code)}", {"deep": deep, "origin": origin})
|
|
881
|
+
return _tariff_selection(await self._client._get(f"/tariff/{_seg(code)}", {"deep": deep, "origin": origin, "edition": edition, "date": date}), edition, date)
|
|
882
|
+
|
|
883
|
+
async def search(self, query: str, *, edition: Optional[str] = None, date: Optional[str] = None) -> Json:
|
|
884
|
+
return _tariff_selection(await self._client._get("/tariff", {"q": query, "edition": edition, "date": date}), edition, date)
|
|
870
885
|
|
|
871
|
-
async def search(self, query: str) -> Json:
|
|
872
|
-
return await self._client._get("/tariff", {"q": query})
|
|
873
886
|
|
|
874
887
|
|
|
875
888
|
class _DateSync:
|
|
@@ -898,13 +911,21 @@ class _TimeSync:
|
|
|
898
911
|
def __init__(self, client: ParseAPI):
|
|
899
912
|
self._client = client
|
|
900
913
|
|
|
901
|
-
def __call__(self, timezone: Optional[str] = None, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
902
|
-
"""Current local time, UTC by default. With to, offsetless at is source wall time.
|
|
903
|
-
|
|
904
|
-
|
|
914
|
+
def __call__(self, timezone: Optional[str] = None, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None, disambiguation: Optional[str] = None, targets: Optional[list[str]] = None, ip: Optional[str] = None, city: Optional[str] = None, country: Optional[str] = None, state: Optional[str] = None, iata: Optional[str] = None, icao: Optional[str] = None, unlocode: Optional[str] = None, address: Optional[str] = None) -> Json:
|
|
915
|
+
"""Current local time, UTC by default. With to or targets, offsetless at is source wall time.
|
|
916
|
+
disambiguation selects compatible (default), earlier, later, or reject at clock changes.
|
|
917
|
+
Explicit offsets select the instant directly."""
|
|
918
|
+
source = _time_source(timezone, ip=ip, city=city, country=country, state=state, iata=iata, icao=icao, unlocode=unlocode, address=address)
|
|
919
|
+
path = _time_path(timezone)
|
|
920
|
+
return self._client._get(path, {**source, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
905
921
|
|
|
906
|
-
def at(self, lat: float, lon: float, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
907
|
-
return self._client._get("/time", {"lat": lat, "lon": lon, "at": at, "to": to, "deep": deep, "lang": lang})
|
|
922
|
+
def at(self, lat: float, lon: float, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None, disambiguation: Optional[str] = None, targets: Optional[list[str]] = None) -> Json:
|
|
923
|
+
return self._client._get("/time", {"lat": lat, "lon": lon, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def zones(self, query: Optional[str] = None, *, country: Optional[str] = None, area: Optional[str] = None, offset: Optional[str] = None, abbreviation: Optional[str] = None, dst: Optional[bool] = None, observes_dst: Optional[bool] = None, at: Optional[str] = None, details: bool = False, sort: Optional[str] = None) -> Json:
|
|
927
|
+
"""Search serving timezone IDs. Omit query to list all."""
|
|
928
|
+
return self._client._get("/time/zones", {"q": query, "country": country, "area": area, "offset": offset, "abbreviation": abbreviation, "dst": None if dst is None else str(dst).lower(), "observes_dst": None if observes_dst is None else str(observes_dst).lower(), "at": at, "details": details, "sort": sort})
|
|
908
929
|
|
|
909
930
|
|
|
910
931
|
class _TimezoneSync:
|
|
@@ -938,13 +959,21 @@ class _TimeAsync:
|
|
|
938
959
|
def __init__(self, client: AsyncParseAPI):
|
|
939
960
|
self._client = client
|
|
940
961
|
|
|
941
|
-
async def __call__(self, timezone: Optional[str] = None, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None) -> Json:
|
|
942
|
-
"""Current local time, UTC by default. With to, offsetless at is source wall time.
|
|
943
|
-
|
|
944
|
-
|
|
962
|
+
async def __call__(self, timezone: Optional[str] = None, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None, disambiguation: Optional[str] = None, targets: Optional[list[str]] = None, ip: Optional[str] = None, city: Optional[str] = None, country: Optional[str] = None, state: Optional[str] = None, iata: Optional[str] = None, icao: Optional[str] = None, unlocode: Optional[str] = None, address: Optional[str] = None) -> Json:
|
|
963
|
+
"""Current local time, UTC by default. With to or targets, offsetless at is source wall time.
|
|
964
|
+
disambiguation selects compatible (default), earlier, later, or reject at clock changes.
|
|
965
|
+
Explicit offsets select the instant directly."""
|
|
966
|
+
source = _time_source(timezone, ip=ip, city=city, country=country, state=state, iata=iata, icao=icao, unlocode=unlocode, address=address)
|
|
967
|
+
path = _time_path(timezone)
|
|
968
|
+
return await self._client._get(path, {**source, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
969
|
+
|
|
970
|
+
async def at(self, lat: float, lon: float, *, at: Optional[str] = None, to: Optional[str] = None, deep: bool = False, lang: Optional[str] = None, disambiguation: Optional[str] = None, targets: Optional[list[str]] = None) -> Json:
|
|
971
|
+
return await self._client._get("/time", {"lat": lat, "lon": lon, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
945
972
|
|
|
946
|
-
|
|
947
|
-
|
|
973
|
+
|
|
974
|
+
async def zones(self, query: Optional[str] = None, *, country: Optional[str] = None, area: Optional[str] = None, offset: Optional[str] = None, abbreviation: Optional[str] = None, dst: Optional[bool] = None, observes_dst: Optional[bool] = None, at: Optional[str] = None, details: bool = False, sort: Optional[str] = None) -> Json:
|
|
975
|
+
"""Search serving timezone IDs. Omit query to list all."""
|
|
976
|
+
return await self._client._get("/time/zones", {"q": query, "country": country, "area": area, "offset": offset, "abbreviation": abbreviation, "dst": None if dst is None else str(dst).lower(), "observes_dst": None if observes_dst is None else str(observes_dst).lower(), "at": at, "details": details, "sort": sort})
|
|
948
977
|
|
|
949
978
|
|
|
950
979
|
class _TimezoneAsync:
|
|
@@ -1004,3 +1033,28 @@ class _MeasureAsync:
|
|
|
1004
1033
|
async def units(self, *, query: Optional[str] = None, type: Optional[str] = None, unit: Optional[str] = None, lang: Optional[str] = None) -> Json:
|
|
1005
1034
|
"""Discover reviewed units. unit filters compatible conversion targets."""
|
|
1006
1035
|
return await self._client._get("/measure/units", {"q": query, "type": type, "unit": unit, "lang": lang})
|
|
1036
|
+
|
|
1037
|
+
def _time_source(timezone, **values):
|
|
1038
|
+
primary = [values.get(key) for key in ("ip", "city", "iata", "icao", "unlocode", "address") if values.get(key) is not None]
|
|
1039
|
+
present = any(value is not None for value in values.values())
|
|
1040
|
+
if (any(value is not None and (not isinstance(value, str) or not value.strip()) for value in values.values())
|
|
1041
|
+
or (timezone is not None and present) or len(primary) > 1
|
|
1042
|
+
or (values.get("country") is not None and primary and values.get("city") is None and values.get("address") is None)
|
|
1043
|
+
or (values.get("state") is not None and ((values.get("city") is None and values.get("address") is None) or values.get("country") is None))
|
|
1044
|
+
or (values.get("address") is not None and values.get("country") is None)):
|
|
1045
|
+
raise ValueError('Pass one Time source, using country only with city or address and state only with city or address and country.')
|
|
1046
|
+
return values
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _time_path(timezone: Optional[str]) -> str:
|
|
1050
|
+
if timezone is not None and timezone.strip().lower() in ("zones", "help"):
|
|
1051
|
+
raise ValueError("Time source must be an IANA timezone ID. Use timezone discovery to list IDs.")
|
|
1052
|
+
return "/time" if timezone is None else f"/time/{_seg(timezone)}"
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _time_targets(targets: Optional[list[str]], to: Optional[str]) -> Optional[str]:
|
|
1056
|
+
if targets is None:
|
|
1057
|
+
return None
|
|
1058
|
+
if to is not None or not isinstance(targets, (list, tuple)) or not 1 <= len(targets) <= 10 or any(not isinstance(zone, str) or not zone.strip() or ',' in zone for zone in targets):
|
|
1059
|
+
raise ValueError("Time targets requires 1 to 10 timezone IDs and cannot be combined with to.")
|
|
1060
|
+
return ','.join(targets)
|
|
@@ -23,6 +23,18 @@ def ok(body=None):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
URL_TABLE = [
|
|
26
|
+
(lambda p: p.time("America/New_York", at="2026-11-01T01:30:00", to="UTC", disambiguation="compatible"), "https://api.parseapi.com/time/America%2FNew_York?at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=compatible"),
|
|
27
|
+
(lambda p: p.time.at(40.71, -74.01, at="2026-11-01T01:30:00", to="UTC", disambiguation="compatible"), "https://api.parseapi.com/time?lat=40.71&lon=-74.01&at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=compatible"),
|
|
28
|
+
(lambda p: p.time("America/New_York", at="2026-11-01T01:30:00", to="UTC", disambiguation="earlier"), "https://api.parseapi.com/time/America%2FNew_York?at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=earlier"),
|
|
29
|
+
(lambda p: p.time.at(40.71, -74.01, at="2026-11-01T01:30:00", to="UTC", disambiguation="earlier"), "https://api.parseapi.com/time?lat=40.71&lon=-74.01&at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=earlier"),
|
|
30
|
+
(lambda p: p.time("America/New_York", at="2026-11-01T01:30:00", to="UTC", disambiguation="later"), "https://api.parseapi.com/time/America%2FNew_York?at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=later"),
|
|
31
|
+
(lambda p: p.time.at(40.71, -74.01, at="2026-11-01T01:30:00", to="UTC", disambiguation="later"), "https://api.parseapi.com/time?lat=40.71&lon=-74.01&at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=later"),
|
|
32
|
+
(lambda p: p.time("America/New_York", at="2026-11-01T01:30:00", to="UTC", disambiguation="reject"), "https://api.parseapi.com/time/America%2FNew_York?at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=reject"),
|
|
33
|
+
(lambda p: p.time.at(40.71, -74.01, at="2026-11-01T01:30:00", to="UTC", disambiguation="reject"), "https://api.parseapi.com/time?lat=40.71&lon=-74.01&at=2026-11-01T01%3A30%3A00&to=UTC&disambiguation=reject"),
|
|
34
|
+
(lambda p: p.time.zones(), "https://api.parseapi.com/time/zones"),
|
|
35
|
+
(lambda p: p.time.zones('Europe'), "https://api.parseapi.com/time/zones?q=Europe"),
|
|
36
|
+
(lambda p: p.time('UTC', targets=['UTC', 'Asia/Tokyo', 'UTC']), "https://api.parseapi.com/time/UTC?targets=UTC%2CAsia%2FTokyo%2CUTC"),
|
|
37
|
+
(lambda p: p.time.at(0, 0, targets=['UTC', 'Asia/Tokyo', 'UTC']), "https://api.parseapi.com/time?lat=0&lon=0&targets=UTC%2CAsia%2FTokyo%2CUTC"),
|
|
26
38
|
(lambda p: p.bin("001234"), "https://api.parseapi.com/bin/001234"),
|
|
27
39
|
(lambda p: p.bin("00 1234-56", deep=True), "https://api.parseapi.com/bin/00%201234-56?deep=true"),
|
|
28
40
|
(lambda p: p.time(), "https://api.parseapi.com/time"),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from parseapi import AsyncParseAPI, ParseAPI, ParseAPIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
10
|
+
@pytest.mark.parametrize("extra", [{}, {"lineage": None}, {"lineage": []}, {"lineage": ["Live horses", "Other horses"]}])
|
|
11
|
+
def test_tariff_search_preserves_parent_context_and_older_results(async_client, extra):
|
|
12
|
+
body = {"q": "horses & ponies", "revision": "fixture", "lines": [{"hts": "0101.29.00.90", "description": "Other", "general": None, **extra, "future": True}]}
|
|
13
|
+
|
|
14
|
+
def respond(request):
|
|
15
|
+
assert request.url.path == "/tariff"
|
|
16
|
+
assert dict(request.url.params) == {"q": "horses & ponies"}
|
|
17
|
+
return httpx.Response(200, json=body)
|
|
18
|
+
|
|
19
|
+
transport = httpx.MockTransport(respond)
|
|
20
|
+
if async_client:
|
|
21
|
+
async def run():
|
|
22
|
+
async with AsyncParseAPI("test", transport=transport) as client:
|
|
23
|
+
assert await client.tariff.search("horses & ponies") == body
|
|
24
|
+
asyncio.run(run())
|
|
25
|
+
else:
|
|
26
|
+
with ParseAPI("test", transport=transport) as client:
|
|
27
|
+
assert client.tariff.search("horses & ponies") == body
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
30
|
+
def test_tariff_edition_date_roundtrip(async_client):
|
|
31
|
+
edition = "a" * 64
|
|
32
|
+
body = {"hts": "0101", "revision": "fixture", "edition": edition, "date": "2026-09-15", "deep": {"effective_rate": None, "reason": "future_reason", "measures": []}}
|
|
33
|
+
seen = []
|
|
34
|
+
|
|
35
|
+
def respond(request):
|
|
36
|
+
seen.append(dict(request.url.params))
|
|
37
|
+
return httpx.Response(200, json={**body, "date": request.url.params.get("date")})
|
|
38
|
+
|
|
39
|
+
transport = httpx.MockTransport(respond)
|
|
40
|
+
if async_client:
|
|
41
|
+
async def run():
|
|
42
|
+
async with AsyncParseAPI("test", transport=transport) as client:
|
|
43
|
+
assert await client.tariff("0101", deep=True, origin="CA", edition=edition, date="2026-09-15") == body
|
|
44
|
+
assert await client.tariff.search("horses", edition=edition, date="2026-09-15") == body
|
|
45
|
+
await client.tariff("0101", edition=edition)
|
|
46
|
+
asyncio.run(run())
|
|
47
|
+
else:
|
|
48
|
+
with ParseAPI("test", transport=transport) as client:
|
|
49
|
+
assert client.tariff("0101", deep=True, origin="CA", edition=edition, date="2026-09-15") == body
|
|
50
|
+
assert client.tariff.search("horses", edition=edition, date="2026-09-15") == body
|
|
51
|
+
client.tariff("0101", edition=edition)
|
|
52
|
+
assert seen == [{"deep": "true", "origin": "CA", "edition": edition, "date": "2026-09-15"}, {"q": "horses", "edition": edition, "date": "2026-09-15"}, {"edition": edition}]
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
55
|
+
def test_tariff_rejects_ignored_selection(async_client):
|
|
56
|
+
transport = httpx.MockTransport(lambda request: httpx.Response(200, json={"revision": "old"}))
|
|
57
|
+
if async_client:
|
|
58
|
+
async def run():
|
|
59
|
+
async with AsyncParseAPI("test", transport=transport) as client:
|
|
60
|
+
with pytest.raises(Exception, match="did not confirm"):
|
|
61
|
+
await client.tariff("0101", edition="a" * 64)
|
|
62
|
+
with pytest.raises(Exception, match="did not confirm"):
|
|
63
|
+
await client.tariff.search("horses", date="2026-09-15")
|
|
64
|
+
asyncio.run(run())
|
|
65
|
+
else:
|
|
66
|
+
with ParseAPI("test", transport=transport) as client:
|
|
67
|
+
with pytest.raises(Exception, match="did not confirm"):
|
|
68
|
+
client.tariff("0101", edition="a" * 64)
|
|
69
|
+
with pytest.raises(Exception, match="did not confirm"):
|
|
70
|
+
client.tariff.search("horses", date="2026-09-15")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
74
|
+
@pytest.mark.parametrize("edition", ["legacy", "", "A" * 64, "a" * 64 + "\n"])
|
|
75
|
+
def test_tariff_rejects_invalid_returned_edition(async_client, edition):
|
|
76
|
+
transport = httpx.MockTransport(lambda request: httpx.Response(200, json={"edition": edition, "date": "2026-09-15"}))
|
|
77
|
+
if async_client:
|
|
78
|
+
async def run():
|
|
79
|
+
async with AsyncParseAPI("test", transport=transport) as client:
|
|
80
|
+
for operation, value in [(client.tariff, "0101"), (client.tariff.search, "horses")]:
|
|
81
|
+
with pytest.raises(ParseAPIError) as error:
|
|
82
|
+
await operation(value, date="2026-09-15")
|
|
83
|
+
assert error.value.code == "tariff_selection_mismatch"
|
|
84
|
+
assert error.value.status == 0
|
|
85
|
+
asyncio.run(run())
|
|
86
|
+
else:
|
|
87
|
+
with ParseAPI("test", transport=transport) as client:
|
|
88
|
+
for operation, value in [(client.tariff, "0101"), (client.tariff.search, "horses")]:
|
|
89
|
+
with pytest.raises(ParseAPIError) as error:
|
|
90
|
+
operation(value, date="2026-09-15")
|
|
91
|
+
assert error.value.code == "tariff_selection_mismatch"
|
|
92
|
+
assert error.value.status == 0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
import pytest
|
|
4
|
+
from parseapi import ParseAPI, AsyncParseAPI
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize('async_mode', [False, True])
|
|
7
|
+
def test_targets_validate_before_dispatch_and_preserve_observations(async_mode):
|
|
8
|
+
calls = []
|
|
9
|
+
payload = {'targets': None}
|
|
10
|
+
def handler(request):
|
|
11
|
+
calls.append(request)
|
|
12
|
+
return httpx.Response(200, json=payload)
|
|
13
|
+
async def run():
|
|
14
|
+
client_type = AsyncParseAPI if async_mode else ParseAPI
|
|
15
|
+
client = client_type('fixture', transport=httpx.MockTransport(handler), retries=0)
|
|
16
|
+
async def invoke(operation, *args, **kwargs):
|
|
17
|
+
result = operation(*args, **kwargs)
|
|
18
|
+
return await result if async_mode else result
|
|
19
|
+
try:
|
|
20
|
+
for targets in [[], [''], [' '], ['UTC,UTC'], ['UTC'] * 11, [None], 'UTC']:
|
|
21
|
+
for method, args in [(client.time, ['UTC']), (client.time.at, [0, 0])]:
|
|
22
|
+
with pytest.raises(ValueError):
|
|
23
|
+
await invoke(method, *args, targets=targets)
|
|
24
|
+
with pytest.raises(ValueError):
|
|
25
|
+
await invoke(client.time, 'UTC', targets=['UTC'], to='UTC')
|
|
26
|
+
assert calls == []
|
|
27
|
+
for value in [None, [], [{'timezone': 'UTC', 'unix': 0}, {'timezone': 'UTC', 'unix': 0}]]:
|
|
28
|
+
payload['targets'] = value
|
|
29
|
+
assert await invoke(client.time, 'UTC', targets=['UTC', 'Asia/Tokyo', 'UTC']) == payload
|
|
30
|
+
assert calls[-1].url.params['targets'] == 'UTC,Asia/Tokyo,UTC'
|
|
31
|
+
finally:
|
|
32
|
+
if async_mode:
|
|
33
|
+
await client.close()
|
|
34
|
+
else:
|
|
35
|
+
client.close()
|
|
36
|
+
asyncio.run(run())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize('async_mode', [False, True])
|
|
40
|
+
def test_reserved_time_source_does_not_call_discovery(async_mode):
|
|
41
|
+
calls = []
|
|
42
|
+
async def run():
|
|
43
|
+
cls = AsyncParseAPI if async_mode else ParseAPI
|
|
44
|
+
parse = cls('fixture', transport=httpx.MockTransport(lambda r: calls.append(r) or httpx.Response(200, json={})))
|
|
45
|
+
try:
|
|
46
|
+
for zone in ['zones', 'help', ' ZONES ', 'Help']:
|
|
47
|
+
with pytest.raises(ValueError, match='IANA timezone ID'):
|
|
48
|
+
result = parse.time(zone)
|
|
49
|
+
if async_mode: await result
|
|
50
|
+
assert calls == []
|
|
51
|
+
finally:
|
|
52
|
+
if async_mode: await parse.close()
|
|
53
|
+
else: parse.close()
|
|
54
|
+
asyncio.run(run())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
import pytest
|
|
4
|
+
from parseapi import ParseAPI, AsyncParseAPI
|
|
5
|
+
RICH = {'timezone_database_version': '2026c', 'timezones': ['UTC'], 'at': '1970-01-01T00:00:00.000Z', 'zones': [{'timezone': 'UTC', 'countries': [], 'area': None, 'abbreviation': 'UTC', 'offset': '+00:00', 'offset_seconds': 0, 'dst': False, 'observes_dst': False}]}
|
|
6
|
+
LOCATED = {'timezone': None, 'targets': None, 'location': {'input': {'type': 'city', 'value': 'Springfield'}, 'status': 'ambiguous', 'candidates': [{'id': 'city_a', 'name': 'Springfield', 'country': 'US', 'state': 'IL', 'timezone': 'America/Chicago', 'latitude': 0, 'longitude': 0}], 'truncated': False, 'source': 'city_reference'}, 'deep': {'standard_offset': '+01:00', 'standard_offset_seconds': 3600, 'dst_offset_seconds': -3600, 'season': {'start': {'at': '2026-10-25T01:00:00Z', 'before': {'offset_seconds': 3600, 'dst': False}, 'after': {'offset_seconds': 0, 'dst': True}, 'change_seconds': -3600}, 'end': None}}}
|
|
7
|
+
@pytest.mark.parametrize('async_mode', [False, True])
|
|
8
|
+
def test_time_explicit_locations_catalog_filters_and_observations(async_mode):
|
|
9
|
+
calls=[]
|
|
10
|
+
def handle(request):
|
|
11
|
+
calls.append(request)
|
|
12
|
+
return httpx.Response(200,json=RICH if request.url.path=='/time/zones' else LOCATED)
|
|
13
|
+
async def run():
|
|
14
|
+
client=(AsyncParseAPI if async_mode else ParseAPI)('fixture',transport=httpx.MockTransport(handle),retries=0)
|
|
15
|
+
async def call(method,*args,**kwargs):
|
|
16
|
+
result=method(*args,**kwargs)
|
|
17
|
+
return await result if async_mode else result
|
|
18
|
+
try:
|
|
19
|
+
assert await call(client.time.zones,country='US',area='America',offset='+00:00',abbreviation='UTC',dst=False,observes_dst=False,at='1970-01-01T00:00:00Z',details=True,sort='offset') == RICH
|
|
20
|
+
assert dict(calls[-1].url.params) == {'country': 'US', 'area': 'America', 'offset': '+00:00', 'abbreviation': 'UTC', 'dst': 'false', 'observes_dst': 'false', 'at': '1970-01-01T00:00:00Z', 'details': 'true', 'sort': 'offset'}
|
|
21
|
+
for source in [{'ip':'2001:db8::1'},{'city':'Springfield','country':'US','state':'IL'},{'country':'US'},{'iata':'JFK'},{'icao':'KJFK'},{'unlocode':'US NYC'},{'address':'1 Main Street','country':'US','state':'NY'}]:
|
|
22
|
+
assert await call(client.time,**source,targets=['UTC'],deep=True) == LOCATED
|
|
23
|
+
assert calls[-1].url.path == '/time'
|
|
24
|
+
for key,value in source.items(): assert calls[-1].url.params[key] == value
|
|
25
|
+
count=len(calls)
|
|
26
|
+
for source in [{'ip':'8.8.8.8','city':'Paris'},{'ip':'8.8.8.8','country':'US'},{'state':'NY'},{'city':'Paris','state':'IDF'},{'address':'a'},{'ip':''}]:
|
|
27
|
+
with pytest.raises(ValueError): await call(client.time,**source)
|
|
28
|
+
with pytest.raises(ValueError): await call(client.time,'UTC',city='Paris')
|
|
29
|
+
assert len(calls)==count
|
|
30
|
+
finally:
|
|
31
|
+
if async_mode: await client.close()
|
|
32
|
+
else: client.close()
|
|
33
|
+
asyncio.run(run())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|