parseapi 1.6.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.6.0 → parseapi-1.7.0}/CHANGELOG.md +4 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/PKG-INFO +8 -2
- {parseapi-1.6.0 → parseapi-1.7.0}/README.md +7 -1
- {parseapi-1.6.0 → parseapi-1.7.0}/api/public.json +48 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/pyproject.toml +1 -1
- {parseapi-1.6.0 → parseapi-1.7.0}/src/parseapi/__init__.py +1 -1
- {parseapi-1.6.0 → parseapi-1.7.0}/src/parseapi/_client.py +22 -9
- parseapi-1.7.0/tests/test_tariff.py +92 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/.github/workflows/publish.yml +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/.github/workflows/test.yml +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/.gitignore +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/LICENSE +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/scripts/public_api.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/smoke/smoke.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/src/parseapi/py.typed +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_client.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_email_enrichment.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_language.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_name_formatting.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_postal_localities.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_public_api.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_stack.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_time_complete.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_time_gaps.py +0 -0
- {parseapi-1.6.0 → parseapi-1.7.0}/tests/test_version.py +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
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
|
+
|
|
3
7
|
## 1.6.0 - 2026-09-24
|
|
4
8
|
|
|
5
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.
|
|
@@ -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
|
|
|
@@ -250,6 +250,12 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
|
|
|
250
250
|
|
|
251
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.
|
|
252
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
|
+
|
|
253
259
|
```python
|
|
254
260
|
parse.tariff("8471.30.01.00")
|
|
255
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
|
|
|
@@ -231,6 +231,12 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
|
|
|
231
231
|
|
|
232
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.
|
|
233
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
|
+
|
|
234
240
|
```python
|
|
235
241
|
parse.tariff("8471.30.01.00")
|
|
236
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"
|
|
@@ -3039,6 +3063,18 @@
|
|
|
3039
3063
|
"kind": "KEYWORD_ONLY",
|
|
3040
3064
|
"name": "origin",
|
|
3041
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]"
|
|
3042
3078
|
}
|
|
3043
3079
|
],
|
|
3044
3080
|
"returns": "Json"
|
|
@@ -3050,6 +3086,18 @@
|
|
|
3050
3086
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
3051
3087
|
"name": "query",
|
|
3052
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]"
|
|
3053
3101
|
}
|
|
3054
3102
|
],
|
|
3055
3103
|
"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:
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|