parseapi 1.9.1__tar.gz → 1.9.3__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.9.1 → parseapi-1.9.3}/PKG-INFO +4 -2
- {parseapi-1.9.1 → parseapi-1.9.3}/README.md +3 -1
- {parseapi-1.9.1 → parseapi-1.9.3}/pyproject.toml +1 -1
- {parseapi-1.9.1 → parseapi-1.9.3}/src/parseapi/__init__.py +1 -1
- {parseapi-1.9.1 → parseapi-1.9.3}/src/parseapi/_client.py +8 -2
- parseapi-1.9.3/tests/fixtures/measure-response.json +51 -0
- parseapi-1.9.3/tests/test_measure_response.py +18 -0
- parseapi-1.9.3/tests/test_tariff_short_id.py +92 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/.github/workflows/publish.yml +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/.github/workflows/test.yml +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/.gitignore +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/CHANGELOG.md +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/LICENSE +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/api/public.json +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/scripts/public_api.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/smoke/smoke.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/src/parseapi/py.typed +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/bank-fixtures.json +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/fixtures/emoji-artwork.json +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/fixtures/emoji-catalog.json +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_bank.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_card_dx.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_client.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_company_directory.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_email_enrichment.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_emoji.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_language.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_name_formatting.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_postal_localities.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_public_api.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_stack.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_tariff.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_time_complete.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_time_gaps.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/test_version.py +0 -0
- {parseapi-1.9.1 → parseapi-1.9.3}/tests/typing_company_directory.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: parseapi
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
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
|
|
@@ -260,6 +260,8 @@ units = parse.measure.units(unit="m")
|
|
|
260
260
|
|
|
261
261
|
`amount` is a decimal string, such as `"180.34"`. Without `to`, the API returns the canonical unit for the measurement type. Pass `locale` for number formatting and `system` (`us` or `imperial`) when a customary unit needs context. Ambiguous input returns `valid: false`, a `reason`, and available `choices`. Invalid or incompatible target units use the normal API error.
|
|
262
262
|
|
|
263
|
+
Successful HTTP JSON contains `measure`, `valid`, `type`, `amount`, and `unit`. Invalid input adds `reason`. `choices` appears only when unit alternatives are available.
|
|
264
|
+
|
|
263
265
|
Unit discovery accepts optional `query`, `type`, and `unit` filters. `unit` selects compatible targets. Omit the filters for the reviewed catalog. Both operations use pooled requests.
|
|
264
266
|
|
|
265
267
|
## Place statistics and optional detail
|
|
@@ -287,7 +289,7 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
|
|
|
287
289
|
|
|
288
290
|
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.
|
|
289
291
|
|
|
290
|
-
Tariff lookup and search accept
|
|
292
|
+
Tariff lookup and search accept the `edition` ID returned by Tariff, such as `ted_3`, and an optional `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.
|
|
291
293
|
|
|
292
294
|
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.
|
|
293
295
|
|
|
@@ -241,6 +241,8 @@ units = parse.measure.units(unit="m")
|
|
|
241
241
|
|
|
242
242
|
`amount` is a decimal string, such as `"180.34"`. Without `to`, the API returns the canonical unit for the measurement type. Pass `locale` for number formatting and `system` (`us` or `imperial`) when a customary unit needs context. Ambiguous input returns `valid: false`, a `reason`, and available `choices`. Invalid or incompatible target units use the normal API error.
|
|
243
243
|
|
|
244
|
+
Successful HTTP JSON contains `measure`, `valid`, `type`, `amount`, and `unit`. Invalid input adds `reason`. `choices` appears only when unit alternatives are available.
|
|
245
|
+
|
|
244
246
|
Unit discovery accepts optional `query`, `type`, and `unit` filters. `unit` selects compatible targets. Omit the filters for the reviewed catalog. Both operations use pooled requests.
|
|
245
247
|
|
|
246
248
|
## Place statistics and optional detail
|
|
@@ -268,7 +270,7 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
|
|
|
268
270
|
|
|
269
271
|
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.
|
|
270
272
|
|
|
271
|
-
Tariff lookup and search accept
|
|
273
|
+
Tariff lookup and search accept the `edition` ID returned by Tariff, such as `ted_3`, and an optional `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.
|
|
272
274
|
|
|
273
275
|
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.
|
|
274
276
|
|
|
@@ -12,7 +12,7 @@ from urllib.parse import quote
|
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
|
|
15
|
-
VERSION = "1.9.
|
|
15
|
+
VERSION = "1.9.3"
|
|
16
16
|
_API_VERSION = "2.0.0"
|
|
17
17
|
DEFAULT_BASE_URL = "https://api.parseapi.com"
|
|
18
18
|
DEFAULT_TIMEOUT = 10.0
|
|
@@ -591,9 +591,13 @@ class _IndustrySync:
|
|
|
591
591
|
return self._client._get("/industry", {"q": query, "limit": limit, "deep": deep})
|
|
592
592
|
|
|
593
593
|
|
|
594
|
+
def _tariff_edition_id(value: Any) -> bool:
|
|
595
|
+
return isinstance(value, str) and (re.fullmatch(r"[a-f0-9]{64}", value) is not None or (re.fullmatch(r"ted_[1-9][0-9]{0,19}", value) is not None and (len(value) < 24 or value[4:] <= "18446744073709551615")))
|
|
596
|
+
|
|
597
|
+
|
|
594
598
|
def _tariff_selection(result: Json, edition: Optional[str], date: Optional[str]) -> Json:
|
|
595
599
|
if (edition is not None or date is not None) and (
|
|
596
|
-
not
|
|
600
|
+
not _tariff_edition_id(result.get("edition"))
|
|
597
601
|
or (edition is not None and result.get("edition") != edition)
|
|
598
602
|
or (result.get("date") != date)
|
|
599
603
|
):
|
|
@@ -1176,6 +1180,7 @@ class _MeasureSync:
|
|
|
1176
1180
|
def __call__(self, measure: str, *, to: Optional[str] = None, locale: Optional[str] = None, system: Optional[str] = None) -> Json:
|
|
1177
1181
|
"""Parse or convert a measurement. Amount is a decimal string. Without to, use the
|
|
1178
1182
|
type's canonical unit. Locale and system (us or imperial) resolve explicit ambiguity.
|
|
1183
|
+
Successful JSON omits reason and choices. Invalid input adds reason and alternatives add choices.
|
|
1179
1184
|
Invalid measurements return valid=False and a reason. Invalid targets raise an API error.
|
|
1180
1185
|
"""
|
|
1181
1186
|
return self._client._get(f"/measure/{_seg(measure)}", {"to": to, "locale": locale, "system": system})
|
|
@@ -1192,6 +1197,7 @@ class _MeasureAsync:
|
|
|
1192
1197
|
async def __call__(self, measure: str, *, to: Optional[str] = None, locale: Optional[str] = None, system: Optional[str] = None) -> Json:
|
|
1193
1198
|
"""Parse or convert a measurement. Amount is a decimal string. Without to, use the
|
|
1194
1199
|
type's canonical unit. Locale and system (us or imperial) resolve explicit ambiguity.
|
|
1200
|
+
Successful JSON omits reason and choices. Invalid input adds reason and alternatives add choices.
|
|
1195
1201
|
Invalid measurements return valid=False and a reason. Invalid targets raise an API error.
|
|
1196
1202
|
"""
|
|
1197
1203
|
return await self._client._get(f"/measure/{_seg(measure)}", {"to": to, "locale": locale, "system": system})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"measure": "0 m",
|
|
4
|
+
"valid": true,
|
|
5
|
+
"type": "length",
|
|
6
|
+
"amount": "0",
|
|
7
|
+
"unit": "m"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"measure": "1.00000000000000000001 m",
|
|
11
|
+
"valid": true,
|
|
12
|
+
"type": "length",
|
|
13
|
+
"amount": "1",
|
|
14
|
+
"unit": "m"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"measure": "180",
|
|
18
|
+
"valid": false,
|
|
19
|
+
"type": null,
|
|
20
|
+
"amount": null,
|
|
21
|
+
"unit": null,
|
|
22
|
+
"reason": "missing_unit"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"measure": "1 gallon",
|
|
26
|
+
"valid": false,
|
|
27
|
+
"type": null,
|
|
28
|
+
"amount": null,
|
|
29
|
+
"unit": null,
|
|
30
|
+
"reason": "ambiguous_unit",
|
|
31
|
+
"choices": [
|
|
32
|
+
{
|
|
33
|
+
"unit": "gal_us",
|
|
34
|
+
"name": "US gallon"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"unit": "gal_imp",
|
|
38
|
+
"name": "imperial gallon"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"measure": "0 m",
|
|
44
|
+
"valid": true,
|
|
45
|
+
"type": "length",
|
|
46
|
+
"amount": "0",
|
|
47
|
+
"unit": "m",
|
|
48
|
+
"reason": null,
|
|
49
|
+
"choices": []
|
|
50
|
+
}
|
|
51
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from parseapi import AsyncParseAPI, ParseAPI
|
|
7
|
+
|
|
8
|
+
CASES = json.loads((Path(__file__).parent / 'fixtures/measure-response.json').read_text())
|
|
9
|
+
|
|
10
|
+
def test_clean_and_legacy_measure_responses_sync_and_async():
|
|
11
|
+
for body in CASES:
|
|
12
|
+
transport = httpx.MockTransport(lambda request: httpx.Response(200, json=body))
|
|
13
|
+
with ParseAPI('fixture', transport=transport) as client:
|
|
14
|
+
assert client.measure(body['measure']) == body
|
|
15
|
+
async def check():
|
|
16
|
+
async with AsyncParseAPI('fixture', transport=transport) as client:
|
|
17
|
+
assert await client.measure(body['measure']) == body
|
|
18
|
+
asyncio.run(check())
|
|
@@ -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 = "ted_18446744073709551615"
|
|
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="ted_1")
|
|
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="ted_1")
|
|
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, "ted_1\n", "ted_1\r\n", "ted_1 ", "ted_0", "ted_01", "ted_+1", "TED_1", "ted_18446744073709551616"])
|
|
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
|
|
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
|