parseapi 1.9.2__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.
Files changed (36) hide show
  1. {parseapi-1.9.2 → parseapi-1.9.3}/PKG-INFO +2 -2
  2. {parseapi-1.9.2 → parseapi-1.9.3}/README.md +1 -1
  3. {parseapi-1.9.2 → parseapi-1.9.3}/pyproject.toml +1 -1
  4. {parseapi-1.9.2 → parseapi-1.9.3}/src/parseapi/__init__.py +1 -1
  5. {parseapi-1.9.2 → parseapi-1.9.3}/src/parseapi/_client.py +6 -2
  6. parseapi-1.9.3/tests/test_tariff_short_id.py +92 -0
  7. {parseapi-1.9.2 → parseapi-1.9.3}/.github/workflows/publish.yml +0 -0
  8. {parseapi-1.9.2 → parseapi-1.9.3}/.github/workflows/test.yml +0 -0
  9. {parseapi-1.9.2 → parseapi-1.9.3}/.gitignore +0 -0
  10. {parseapi-1.9.2 → parseapi-1.9.3}/CHANGELOG.md +0 -0
  11. {parseapi-1.9.2 → parseapi-1.9.3}/LICENSE +0 -0
  12. {parseapi-1.9.2 → parseapi-1.9.3}/api/public.json +0 -0
  13. {parseapi-1.9.2 → parseapi-1.9.3}/scripts/public_api.py +0 -0
  14. {parseapi-1.9.2 → parseapi-1.9.3}/smoke/smoke.py +0 -0
  15. {parseapi-1.9.2 → parseapi-1.9.3}/src/parseapi/py.typed +0 -0
  16. {parseapi-1.9.2 → parseapi-1.9.3}/tests/bank-fixtures.json +0 -0
  17. {parseapi-1.9.2 → parseapi-1.9.3}/tests/fixtures/emoji-artwork.json +0 -0
  18. {parseapi-1.9.2 → parseapi-1.9.3}/tests/fixtures/emoji-catalog.json +0 -0
  19. {parseapi-1.9.2 → parseapi-1.9.3}/tests/fixtures/measure-response.json +0 -0
  20. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_bank.py +0 -0
  21. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_card_dx.py +0 -0
  22. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_client.py +0 -0
  23. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_company_directory.py +0 -0
  24. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_email_enrichment.py +0 -0
  25. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_emoji.py +0 -0
  26. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_language.py +0 -0
  27. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_measure_response.py +0 -0
  28. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_name_formatting.py +0 -0
  29. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_postal_localities.py +0 -0
  30. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_public_api.py +0 -0
  31. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_stack.py +0 -0
  32. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_tariff.py +0 -0
  33. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_time_complete.py +0 -0
  34. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_time_gaps.py +0 -0
  35. {parseapi-1.9.2 → parseapi-1.9.3}/tests/test_version.py +0 -0
  36. {parseapi-1.9.2 → 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.2
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
@@ -289,7 +289,7 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
289
289
 
290
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.
291
291
 
292
- 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.
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.
293
293
 
294
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.
295
295
 
@@ -270,7 +270,7 @@ parse.weather(40.7128, -74.006, deep=True, date="2026-08-15")
270
270
 
271
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.
272
272
 
273
- 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.
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.
274
274
 
275
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.
276
276
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "parseapi"
7
- version = "1.9.2"
7
+ version = "1.9.3"
8
8
  description = "Official ParseAPI client for Python. One key, minimal JSON, fast."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,5 +2,5 @@
2
2
 
3
3
  from ._client import AsyncParseAPI, ParseAPI, ParseAPIError
4
4
 
5
- __version__ = "1.9.2"
5
+ __version__ = "1.9.3"
6
6
  __all__ = ["ParseAPI", "AsyncParseAPI", "ParseAPIError", "__version__"]
@@ -12,7 +12,7 @@ from urllib.parse import quote
12
12
 
13
13
  import httpx
14
14
 
15
- VERSION = "1.9.0"
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 isinstance(result.get("edition"), str) or re.fullmatch(r"[a-f0-9]{64}", result["edition"]) is None
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
  ):
@@ -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