parseapi 1.3.0__tar.gz → 1.6.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.3.0 → parseapi-1.6.0}/CHANGELOG.md +8 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/PKG-INFO +47 -3
- {parseapi-1.3.0 → parseapi-1.6.0}/README.md +46 -2
- {parseapi-1.3.0 → parseapi-1.6.0}/api/public.json +276 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/pyproject.toml +1 -1
- {parseapi-1.3.0 → parseapi-1.6.0}/src/parseapi/__init__.py +1 -1
- {parseapi-1.3.0 → parseapi-1.6.0}/src/parseapi/_client.py +54 -13
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_client.py +12 -0
- parseapi-1.6.0/tests/test_postal_localities.py +25 -0
- parseapi-1.6.0/tests/test_time_complete.py +54 -0
- parseapi-1.6.0/tests/test_time_gaps.py +33 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/.github/workflows/publish.yml +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/.github/workflows/test.yml +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/.gitignore +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/LICENSE +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/scripts/public_api.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/smoke/smoke.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/src/parseapi/py.typed +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_email_enrichment.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_language.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_name_formatting.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_public_api.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_stack.py +0 -0
- {parseapi-1.3.0 → parseapi-1.6.0}/tests/test_version.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.0 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## 1.4.0 - 2026-09-24
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
3
11
|
## 1.3.0 - Unreleased
|
|
4
12
|
|
|
5
13
|
Adds Stack site inventory lookup with eight technology category arrays, nullable versions, and explicit page coverage. Stack uses a 35-second default attempt timeout while explicit caller settings remain honored. Existing calls 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.6.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.6.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
|
|
|
@@ -185,6 +227,8 @@ Unit discovery accepts optional `query`, `type`, and `unit` filters. `unit` sele
|
|
|
185
227
|
|
|
186
228
|
## Place statistics and optional detail
|
|
187
229
|
|
|
230
|
+
Australian postal lookups include core `localities` with suburb choices (`city`, `state`, `state_name`) on every plan. Null or an omitted field means unknown, while `[]` means the reviewed reference has no eligible choices. `city` stays null when the source is ambiguous, even if there is only one eligible choice. Let the user select their suburb and keep manual entry available. These are geographic choices, not mailing-address verification. [G-NAF source, adaptations and licence](https://parseapi.com/legal/attribution#postal-au).
|
|
231
|
+
|
|
188
232
|
Postal and District paid profiles include `deep.property_tax` where supported. It contains `annual_median`, `currency` and `period`: median annual property tax payable on owner-occupied homes in the statistical area. The amount is adjusted to the final year of the reporting period (`YYYY-YYYY`). This is an area statistic, not a rate or an individual property bill. Unsupported, missing and censored estimates are null.
|
|
189
233
|
|
|
190
234
|
```python
|
|
@@ -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.6.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
|
|
|
@@ -166,6 +208,8 @@ Unit discovery accepts optional `query`, `type`, and `unit` filters. `unit` sele
|
|
|
166
208
|
|
|
167
209
|
## Place statistics and optional detail
|
|
168
210
|
|
|
211
|
+
Australian postal lookups include core `localities` with suburb choices (`city`, `state`, `state_name`) on every plan. Null or an omitted field means unknown, while `[]` means the reviewed reference has no eligible choices. `city` stays null when the source is ambiguous, even if there is only one eligible choice. Let the user select their suburb and keep manual entry available. These are geographic choices, not mailing-address verification. [G-NAF source, adaptations and licence](https://parseapi.com/legal/attribution#postal-au).
|
|
212
|
+
|
|
169
213
|
Postal and District paid profiles include `deep.property_tax` where supported. It contains `annual_median`, `currency` and `period`: median annual property tax payable on owner-occupied homes in the statistical area. The amount is adjusted to the final year of the reporting period (`YYYY-YYYY`). This is an area statistic, not a rate or an individual property bill. Unsupported, missing and censored estimates are null.
|
|
170
214
|
|
|
171
215
|
```python
|
|
@@ -1371,6 +1371,66 @@
|
|
|
1371
1371
|
"kind": "KEYWORD_ONLY",
|
|
1372
1372
|
"name": "lang",
|
|
1373
1373
|
"type": "Optional[str]"
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
"default": null,
|
|
1377
|
+
"kind": "KEYWORD_ONLY",
|
|
1378
|
+
"name": "disambiguation",
|
|
1379
|
+
"type": "Optional[str]"
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
"default": null,
|
|
1383
|
+
"kind": "KEYWORD_ONLY",
|
|
1384
|
+
"name": "targets",
|
|
1385
|
+
"type": "Optional[list[str]]"
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
"default": null,
|
|
1389
|
+
"kind": "KEYWORD_ONLY",
|
|
1390
|
+
"name": "ip",
|
|
1391
|
+
"type": "Optional[str]"
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
"default": null,
|
|
1395
|
+
"kind": "KEYWORD_ONLY",
|
|
1396
|
+
"name": "city",
|
|
1397
|
+
"type": "Optional[str]"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
"default": null,
|
|
1401
|
+
"kind": "KEYWORD_ONLY",
|
|
1402
|
+
"name": "country",
|
|
1403
|
+
"type": "Optional[str]"
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
"default": null,
|
|
1407
|
+
"kind": "KEYWORD_ONLY",
|
|
1408
|
+
"name": "state",
|
|
1409
|
+
"type": "Optional[str]"
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
"default": null,
|
|
1413
|
+
"kind": "KEYWORD_ONLY",
|
|
1414
|
+
"name": "iata",
|
|
1415
|
+
"type": "Optional[str]"
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
"default": null,
|
|
1419
|
+
"kind": "KEYWORD_ONLY",
|
|
1420
|
+
"name": "icao",
|
|
1421
|
+
"type": "Optional[str]"
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
"default": null,
|
|
1425
|
+
"kind": "KEYWORD_ONLY",
|
|
1426
|
+
"name": "unlocode",
|
|
1427
|
+
"type": "Optional[str]"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
"default": null,
|
|
1431
|
+
"kind": "KEYWORD_ONLY",
|
|
1432
|
+
"name": "address",
|
|
1433
|
+
"type": "Optional[str]"
|
|
1374
1434
|
}
|
|
1375
1435
|
],
|
|
1376
1436
|
"returns": "Json"
|
|
@@ -1411,6 +1471,84 @@
|
|
|
1411
1471
|
"kind": "KEYWORD_ONLY",
|
|
1412
1472
|
"name": "lang",
|
|
1413
1473
|
"type": "Optional[str]"
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
"default": null,
|
|
1477
|
+
"kind": "KEYWORD_ONLY",
|
|
1478
|
+
"name": "disambiguation",
|
|
1479
|
+
"type": "Optional[str]"
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
"default": null,
|
|
1483
|
+
"kind": "KEYWORD_ONLY",
|
|
1484
|
+
"name": "targets",
|
|
1485
|
+
"type": "Optional[list[str]]"
|
|
1486
|
+
}
|
|
1487
|
+
],
|
|
1488
|
+
"returns": "Json"
|
|
1489
|
+
},
|
|
1490
|
+
"time.zones": {
|
|
1491
|
+
"async": true,
|
|
1492
|
+
"parameters": [
|
|
1493
|
+
{
|
|
1494
|
+
"default": null,
|
|
1495
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
1496
|
+
"name": "query",
|
|
1497
|
+
"type": "Optional[str]"
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
"default": null,
|
|
1501
|
+
"kind": "KEYWORD_ONLY",
|
|
1502
|
+
"name": "country",
|
|
1503
|
+
"type": "Optional[str]"
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
"default": null,
|
|
1507
|
+
"kind": "KEYWORD_ONLY",
|
|
1508
|
+
"name": "area",
|
|
1509
|
+
"type": "Optional[str]"
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
"default": null,
|
|
1513
|
+
"kind": "KEYWORD_ONLY",
|
|
1514
|
+
"name": "offset",
|
|
1515
|
+
"type": "Optional[str]"
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
"default": null,
|
|
1519
|
+
"kind": "KEYWORD_ONLY",
|
|
1520
|
+
"name": "abbreviation",
|
|
1521
|
+
"type": "Optional[str]"
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
"default": null,
|
|
1525
|
+
"kind": "KEYWORD_ONLY",
|
|
1526
|
+
"name": "dst",
|
|
1527
|
+
"type": "Optional[bool]"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
"default": null,
|
|
1531
|
+
"kind": "KEYWORD_ONLY",
|
|
1532
|
+
"name": "observes_dst",
|
|
1533
|
+
"type": "Optional[bool]"
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
"default": null,
|
|
1537
|
+
"kind": "KEYWORD_ONLY",
|
|
1538
|
+
"name": "at",
|
|
1539
|
+
"type": "Optional[str]"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
"default": false,
|
|
1543
|
+
"kind": "KEYWORD_ONLY",
|
|
1544
|
+
"name": "details",
|
|
1545
|
+
"type": "bool"
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
"default": null,
|
|
1549
|
+
"kind": "KEYWORD_ONLY",
|
|
1550
|
+
"name": "sort",
|
|
1551
|
+
"type": "Optional[str]"
|
|
1414
1552
|
}
|
|
1415
1553
|
],
|
|
1416
1554
|
"returns": "Json"
|
|
@@ -2948,6 +3086,66 @@
|
|
|
2948
3086
|
"kind": "KEYWORD_ONLY",
|
|
2949
3087
|
"name": "lang",
|
|
2950
3088
|
"type": "Optional[str]"
|
|
3089
|
+
},
|
|
3090
|
+
{
|
|
3091
|
+
"default": null,
|
|
3092
|
+
"kind": "KEYWORD_ONLY",
|
|
3093
|
+
"name": "disambiguation",
|
|
3094
|
+
"type": "Optional[str]"
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
"default": null,
|
|
3098
|
+
"kind": "KEYWORD_ONLY",
|
|
3099
|
+
"name": "targets",
|
|
3100
|
+
"type": "Optional[list[str]]"
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
"default": null,
|
|
3104
|
+
"kind": "KEYWORD_ONLY",
|
|
3105
|
+
"name": "ip",
|
|
3106
|
+
"type": "Optional[str]"
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
"default": null,
|
|
3110
|
+
"kind": "KEYWORD_ONLY",
|
|
3111
|
+
"name": "city",
|
|
3112
|
+
"type": "Optional[str]"
|
|
3113
|
+
},
|
|
3114
|
+
{
|
|
3115
|
+
"default": null,
|
|
3116
|
+
"kind": "KEYWORD_ONLY",
|
|
3117
|
+
"name": "country",
|
|
3118
|
+
"type": "Optional[str]"
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
"default": null,
|
|
3122
|
+
"kind": "KEYWORD_ONLY",
|
|
3123
|
+
"name": "state",
|
|
3124
|
+
"type": "Optional[str]"
|
|
3125
|
+
},
|
|
3126
|
+
{
|
|
3127
|
+
"default": null,
|
|
3128
|
+
"kind": "KEYWORD_ONLY",
|
|
3129
|
+
"name": "iata",
|
|
3130
|
+
"type": "Optional[str]"
|
|
3131
|
+
},
|
|
3132
|
+
{
|
|
3133
|
+
"default": null,
|
|
3134
|
+
"kind": "KEYWORD_ONLY",
|
|
3135
|
+
"name": "icao",
|
|
3136
|
+
"type": "Optional[str]"
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
"default": null,
|
|
3140
|
+
"kind": "KEYWORD_ONLY",
|
|
3141
|
+
"name": "unlocode",
|
|
3142
|
+
"type": "Optional[str]"
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
"default": null,
|
|
3146
|
+
"kind": "KEYWORD_ONLY",
|
|
3147
|
+
"name": "address",
|
|
3148
|
+
"type": "Optional[str]"
|
|
2951
3149
|
}
|
|
2952
3150
|
],
|
|
2953
3151
|
"returns": "Json"
|
|
@@ -2988,6 +3186,84 @@
|
|
|
2988
3186
|
"kind": "KEYWORD_ONLY",
|
|
2989
3187
|
"name": "lang",
|
|
2990
3188
|
"type": "Optional[str]"
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
"default": null,
|
|
3192
|
+
"kind": "KEYWORD_ONLY",
|
|
3193
|
+
"name": "disambiguation",
|
|
3194
|
+
"type": "Optional[str]"
|
|
3195
|
+
},
|
|
3196
|
+
{
|
|
3197
|
+
"default": null,
|
|
3198
|
+
"kind": "KEYWORD_ONLY",
|
|
3199
|
+
"name": "targets",
|
|
3200
|
+
"type": "Optional[list[str]]"
|
|
3201
|
+
}
|
|
3202
|
+
],
|
|
3203
|
+
"returns": "Json"
|
|
3204
|
+
},
|
|
3205
|
+
"time.zones": {
|
|
3206
|
+
"async": false,
|
|
3207
|
+
"parameters": [
|
|
3208
|
+
{
|
|
3209
|
+
"default": null,
|
|
3210
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
3211
|
+
"name": "query",
|
|
3212
|
+
"type": "Optional[str]"
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
"default": null,
|
|
3216
|
+
"kind": "KEYWORD_ONLY",
|
|
3217
|
+
"name": "country",
|
|
3218
|
+
"type": "Optional[str]"
|
|
3219
|
+
},
|
|
3220
|
+
{
|
|
3221
|
+
"default": null,
|
|
3222
|
+
"kind": "KEYWORD_ONLY",
|
|
3223
|
+
"name": "area",
|
|
3224
|
+
"type": "Optional[str]"
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
"default": null,
|
|
3228
|
+
"kind": "KEYWORD_ONLY",
|
|
3229
|
+
"name": "offset",
|
|
3230
|
+
"type": "Optional[str]"
|
|
3231
|
+
},
|
|
3232
|
+
{
|
|
3233
|
+
"default": null,
|
|
3234
|
+
"kind": "KEYWORD_ONLY",
|
|
3235
|
+
"name": "abbreviation",
|
|
3236
|
+
"type": "Optional[str]"
|
|
3237
|
+
},
|
|
3238
|
+
{
|
|
3239
|
+
"default": null,
|
|
3240
|
+
"kind": "KEYWORD_ONLY",
|
|
3241
|
+
"name": "dst",
|
|
3242
|
+
"type": "Optional[bool]"
|
|
3243
|
+
},
|
|
3244
|
+
{
|
|
3245
|
+
"default": null,
|
|
3246
|
+
"kind": "KEYWORD_ONLY",
|
|
3247
|
+
"name": "observes_dst",
|
|
3248
|
+
"type": "Optional[bool]"
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
"default": null,
|
|
3252
|
+
"kind": "KEYWORD_ONLY",
|
|
3253
|
+
"name": "at",
|
|
3254
|
+
"type": "Optional[str]"
|
|
3255
|
+
},
|
|
3256
|
+
{
|
|
3257
|
+
"default": false,
|
|
3258
|
+
"kind": "KEYWORD_ONLY",
|
|
3259
|
+
"name": "details",
|
|
3260
|
+
"type": "bool"
|
|
3261
|
+
},
|
|
3262
|
+
{
|
|
3263
|
+
"default": null,
|
|
3264
|
+
"kind": "KEYWORD_ONLY",
|
|
3265
|
+
"name": "sort",
|
|
3266
|
+
"type": "Optional[str]"
|
|
2991
3267
|
}
|
|
2992
3268
|
],
|
|
2993
3269
|
"returns": "Json"
|
|
@@ -10,7 +10,7 @@ from urllib.parse import quote
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
13
|
-
VERSION = "1.
|
|
13
|
+
VERSION = "1.6.0"
|
|
14
14
|
_API_VERSION = "2.0.0"
|
|
15
15
|
DEFAULT_BASE_URL = "https://api.parseapi.com"
|
|
16
16
|
DEFAULT_TIMEOUT = 10.0
|
|
@@ -898,13 +898,21 @@ class _TimeSync:
|
|
|
898
898
|
def __init__(self, client: ParseAPI):
|
|
899
899
|
self._client = client
|
|
900
900
|
|
|
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
|
-
|
|
901
|
+
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:
|
|
902
|
+
"""Current local time, UTC by default. With to or targets, offsetless at is source wall time.
|
|
903
|
+
disambiguation selects compatible (default), earlier, later, or reject at clock changes.
|
|
904
|
+
Explicit offsets select the instant directly."""
|
|
905
|
+
source = _time_source(timezone, ip=ip, city=city, country=country, state=state, iata=iata, icao=icao, unlocode=unlocode, address=address)
|
|
906
|
+
path = _time_path(timezone)
|
|
907
|
+
return self._client._get(path, {**source, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
905
908
|
|
|
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})
|
|
909
|
+
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:
|
|
910
|
+
return self._client._get("/time", {"lat": lat, "lon": lon, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
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:
|
|
914
|
+
"""Search serving timezone IDs. Omit query to list all."""
|
|
915
|
+
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
916
|
|
|
909
917
|
|
|
910
918
|
class _TimezoneSync:
|
|
@@ -938,13 +946,21 @@ class _TimeAsync:
|
|
|
938
946
|
def __init__(self, client: AsyncParseAPI):
|
|
939
947
|
self._client = client
|
|
940
948
|
|
|
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
|
-
|
|
949
|
+
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:
|
|
950
|
+
"""Current local time, UTC by default. With to or targets, offsetless at is source wall time.
|
|
951
|
+
disambiguation selects compatible (default), earlier, later, or reject at clock changes.
|
|
952
|
+
Explicit offsets select the instant directly."""
|
|
953
|
+
source = _time_source(timezone, ip=ip, city=city, country=country, state=state, iata=iata, icao=icao, unlocode=unlocode, address=address)
|
|
954
|
+
path = _time_path(timezone)
|
|
955
|
+
return await self._client._get(path, {**source, "at": at, "to": to, "deep": deep, "lang": lang, "disambiguation": disambiguation, "targets": _time_targets(targets, to)})
|
|
956
|
+
|
|
957
|
+
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:
|
|
958
|
+
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
959
|
|
|
946
|
-
|
|
947
|
-
|
|
960
|
+
|
|
961
|
+
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:
|
|
962
|
+
"""Search serving timezone IDs. Omit query to list all."""
|
|
963
|
+
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
964
|
|
|
949
965
|
|
|
950
966
|
class _TimezoneAsync:
|
|
@@ -1004,3 +1020,28 @@ class _MeasureAsync:
|
|
|
1004
1020
|
async def units(self, *, query: Optional[str] = None, type: Optional[str] = None, unit: Optional[str] = None, lang: Optional[str] = None) -> Json:
|
|
1005
1021
|
"""Discover reviewed units. unit filters compatible conversion targets."""
|
|
1006
1022
|
return await self._client._get("/measure/units", {"q": query, "type": type, "unit": unit, "lang": lang})
|
|
1023
|
+
|
|
1024
|
+
def _time_source(timezone, **values):
|
|
1025
|
+
primary = [values.get(key) for key in ("ip", "city", "iata", "icao", "unlocode", "address") if values.get(key) is not None]
|
|
1026
|
+
present = any(value is not None for value in values.values())
|
|
1027
|
+
if (any(value is not None and (not isinstance(value, str) or not value.strip()) for value in values.values())
|
|
1028
|
+
or (timezone is not None and present) or len(primary) > 1
|
|
1029
|
+
or (values.get("country") is not None and primary and values.get("city") is None and values.get("address") is None)
|
|
1030
|
+
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))
|
|
1031
|
+
or (values.get("address") is not None and values.get("country") is None)):
|
|
1032
|
+
raise ValueError('Pass one Time source, using country only with city or address and state only with city or address and country.')
|
|
1033
|
+
return values
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _time_path(timezone: Optional[str]) -> str:
|
|
1037
|
+
if timezone is not None and timezone.strip().lower() in ("zones", "help"):
|
|
1038
|
+
raise ValueError("Time source must be an IANA timezone ID. Use timezone discovery to list IDs.")
|
|
1039
|
+
return "/time" if timezone is None else f"/time/{_seg(timezone)}"
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _time_targets(targets: Optional[list[str]], to: Optional[str]) -> Optional[str]:
|
|
1043
|
+
if targets is None:
|
|
1044
|
+
return None
|
|
1045
|
+
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):
|
|
1046
|
+
raise ValueError("Time targets requires 1 to 10 timezone IDs and cannot be combined with to.")
|
|
1047
|
+
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,25 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from parseapi import AsyncParseAPI, ParseAPI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
10
|
+
@pytest.mark.parametrize("extra", [
|
|
11
|
+
{}, {"localities": None}, {"localities": []},
|
|
12
|
+
{"localities": [{"city": "SYDNEY", "state": "NSW", "state_name": "New South Wales", "future": True}]},
|
|
13
|
+
{"localities": [{"city": "SYDNEY", "state": "NSW", "state_name": "New South Wales"}, {"city": "HAYMARKET", "state": "NSW", "state_name": "New South Wales"}]},
|
|
14
|
+
])
|
|
15
|
+
def test_postal_choices_preserve_observation_without_inferring_city(async_client, extra):
|
|
16
|
+
body = {"postal": "2000", "country": "AU", "city": None, **extra}
|
|
17
|
+
transport = httpx.MockTransport(lambda request: httpx.Response(200, json=body))
|
|
18
|
+
if async_client:
|
|
19
|
+
async def run():
|
|
20
|
+
async with AsyncParseAPI("test", transport=transport) as client:
|
|
21
|
+
assert await client.postal("2000", country="AU") == body
|
|
22
|
+
asyncio.run(run())
|
|
23
|
+
else:
|
|
24
|
+
with ParseAPI("test", transport=transport) as client:
|
|
25
|
+
assert client.postal("2000", country="AU") == body
|
|
@@ -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
|