hts-lookup 0.1.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.
- hts_lookup-0.1.0/LICENSE +18 -0
- hts_lookup-0.1.0/PKG-INFO +68 -0
- hts_lookup-0.1.0/README.md +51 -0
- hts_lookup-0.1.0/pyproject.toml +29 -0
- hts_lookup-0.1.0/setup.cfg +4 -0
- hts_lookup-0.1.0/src/hts_lookup/__init__.py +145 -0
- hts_lookup-0.1.0/src/hts_lookup/__main__.py +46 -0
- hts_lookup-0.1.0/src/hts_lookup/data/hts.json.gz +0 -0
- hts_lookup-0.1.0/src/hts_lookup.egg-info/PKG-INFO +68 -0
- hts_lookup-0.1.0/src/hts_lookup.egg-info/SOURCES.txt +12 -0
- hts_lookup-0.1.0/src/hts_lookup.egg-info/dependency_links.txt +1 -0
- hts_lookup-0.1.0/src/hts_lookup.egg-info/entry_points.txt +2 -0
- hts_lookup-0.1.0/src/hts_lookup.egg-info/top_level.txt +1 -0
- hts_lookup-0.1.0/tests/test_hts_lookup.py +42 -0
hts_lookup-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License (code)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anders Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
|
+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
7
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
8
|
+
persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
11
|
+
Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
14
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
15
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
16
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
17
|
+
|
|
18
|
+
The data files in steel_sections/data/ are not covered by this licence; see DATA-LICENSE.md.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: hts-lookup
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes: official descriptions, duty rates, closest valid code. No dependencies.
|
|
5
|
+
Author: Anders Lee
|
|
6
|
+
License: MIT (code); public domain US government data (see DATA-LICENSE.md)
|
|
7
|
+
Project-URL: Homepage, https://hs-code.net/
|
|
8
|
+
Keywords: hts,hs code,harmonized tariff schedule,tariff,customs,duty rate,import,trade,classification,usitc
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Office/Business
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# hts-lookup
|
|
19
|
+
|
|
20
|
+
Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes. The current schedule is bundled
|
|
21
|
+
(about 0.5 MB compressed), so it works without network access or API keys. Pure Python, no dependencies.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install hts-lookup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import hts_lookup as hts
|
|
29
|
+
|
|
30
|
+
hts.TITLE # 'Revision 21 (2026)'
|
|
31
|
+
line = hts.lookup("6201.40.70")
|
|
32
|
+
line.description # 'Water resistant (634)'
|
|
33
|
+
line.general # '7.1%'
|
|
34
|
+
[c for c, _ in line.path] # ['6201', '6201.40', '', '', '', '', '', '6201.40.70.00']
|
|
35
|
+
|
|
36
|
+
hts.lookup("6109.10.00.12").general # '16.5%' (10-digit lines inherit the 8-digit rate)
|
|
37
|
+
hts.lookup("6201.40").declarable # False (a subheading with lines under it)
|
|
38
|
+
hts.lookup("8471.30").code # '8471.30.01.00' (only one line under it, so that line)
|
|
39
|
+
|
|
40
|
+
hts.is_valid("6201.40.99.99") # False
|
|
41
|
+
hts.closest("6201409999").code # '6201.40'
|
|
42
|
+
|
|
43
|
+
[l.code for l in hts.children("8507.60")]
|
|
44
|
+
hts.search("lithium ion", limit=5)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Command line:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
$ hts-lookup 6201.40.70
|
|
51
|
+
$ hts-lookup --search "vinyl stickers"
|
|
52
|
+
$ hts-lookup --children 4202.92
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why validate codes
|
|
56
|
+
|
|
57
|
+
HS and HTS codes copied from supplier invoices, old spreadsheets or AI chat answers are often out of date or do not exist.
|
|
58
|
+
`is_valid()` and `closest()` catch those before a shipment does.
|
|
59
|
+
|
|
60
|
+
## Notes
|
|
61
|
+
|
|
62
|
+
- `general` is the column 1 general (MFN) rate. Extra duties under Chapter 99 depend on the country of origin and are not applied.
|
|
63
|
+
- The first six digits are the international HS code; digits 7 to 10 are US-specific.
|
|
64
|
+
- The bundled schedule is updated with new USITC revisions; check `hts_lookup.TITLE` for the edition you have.
|
|
65
|
+
|
|
66
|
+
Step-by-step product guides, UK and EU rates, and the customs rulings behind each code: [hs-code.net](https://hs-code.net/)
|
|
67
|
+
|
|
68
|
+
Code: MIT. Data: public domain US government work, see DATA-LICENSE.md. Not affiliated with USITC or CBP.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# hts-lookup
|
|
2
|
+
|
|
3
|
+
Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes. The current schedule is bundled
|
|
4
|
+
(about 0.5 MB compressed), so it works without network access or API keys. Pure Python, no dependencies.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
pip install hts-lookup
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import hts_lookup as hts
|
|
12
|
+
|
|
13
|
+
hts.TITLE # 'Revision 21 (2026)'
|
|
14
|
+
line = hts.lookup("6201.40.70")
|
|
15
|
+
line.description # 'Water resistant (634)'
|
|
16
|
+
line.general # '7.1%'
|
|
17
|
+
[c for c, _ in line.path] # ['6201', '6201.40', '', '', '', '', '', '6201.40.70.00']
|
|
18
|
+
|
|
19
|
+
hts.lookup("6109.10.00.12").general # '16.5%' (10-digit lines inherit the 8-digit rate)
|
|
20
|
+
hts.lookup("6201.40").declarable # False (a subheading with lines under it)
|
|
21
|
+
hts.lookup("8471.30").code # '8471.30.01.00' (only one line under it, so that line)
|
|
22
|
+
|
|
23
|
+
hts.is_valid("6201.40.99.99") # False
|
|
24
|
+
hts.closest("6201409999").code # '6201.40'
|
|
25
|
+
|
|
26
|
+
[l.code for l in hts.children("8507.60")]
|
|
27
|
+
hts.search("lithium ion", limit=5)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Command line:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
$ hts-lookup 6201.40.70
|
|
34
|
+
$ hts-lookup --search "vinyl stickers"
|
|
35
|
+
$ hts-lookup --children 4202.92
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Why validate codes
|
|
39
|
+
|
|
40
|
+
HS and HTS codes copied from supplier invoices, old spreadsheets or AI chat answers are often out of date or do not exist.
|
|
41
|
+
`is_valid()` and `closest()` catch those before a shipment does.
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- `general` is the column 1 general (MFN) rate. Extra duties under Chapter 99 depend on the country of origin and are not applied.
|
|
46
|
+
- The first six digits are the international HS code; digits 7 to 10 are US-specific.
|
|
47
|
+
- The bundled schedule is updated with new USITC revisions; check `hts_lookup.TITLE` for the edition you have.
|
|
48
|
+
|
|
49
|
+
Step-by-step product guides, UK and EU rates, and the customs rulings behind each code: [hs-code.net](https://hs-code.net/)
|
|
50
|
+
|
|
51
|
+
Code: MIT. Data: public domain US government work, see DATA-LICENSE.md. Not affiliated with USITC or CBP.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hts-lookup"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes: official descriptions, duty rates, closest valid code. No dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT (code); public domain US government data (see DATA-LICENSE.md)"}
|
|
12
|
+
authors = [{name = "Anders Lee"}]
|
|
13
|
+
keywords = ["hts", "hs code", "harmonized tariff schedule", "tariff", "customs", "duty rate", "import", "trade", "classification", "usitc"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Office/Business",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://hs-code.net/"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
hts-lookup = "hts_lookup.__main__:main"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
hts_lookup = ["data/*.json.gz"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes.
|
|
2
|
+
|
|
3
|
+
>>> import hts_lookup as hts
|
|
4
|
+
>>> hts.lookup("6201.40.70").general
|
|
5
|
+
'7.1%'
|
|
6
|
+
>>> hts.is_valid("6201.40.99.99")
|
|
7
|
+
False
|
|
8
|
+
"""
|
|
9
|
+
import gzip
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = ["Line", "lookup", "is_valid", "closest", "children", "search", "format_code", "RELEASE", "TITLE", "EFFECTIVE"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load():
|
|
21
|
+
with gzip.open(Path(__file__).with_name("data") / "hts.json.gz", "rt", encoding="utf-8") as f:
|
|
22
|
+
return json.load(f)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_DATA = _load()
|
|
26
|
+
RELEASE: str = _DATA["release"] # e.g. "2026HTSRev21"
|
|
27
|
+
TITLE: str = _DATA["title"] # e.g. "Revision 21 (2026)"
|
|
28
|
+
EFFECTIVE: str = _DATA["effective"] # MM/DD/YYYY, as published by USITC
|
|
29
|
+
_ROWS = _DATA["rows"] # [digits, indent, description, general, special, column 2, units]
|
|
30
|
+
_INDEX = {r[0]: i for i, r in enumerate(_ROWS) if r[0]}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Line:
|
|
35
|
+
code: str # dotted, e.g. "6201.40.70.00"
|
|
36
|
+
description: str
|
|
37
|
+
path: List[Tuple[str, str]] # (code or "", description) from the heading down, including unnumbered levels
|
|
38
|
+
general: str # general (MFN) rate that applies, inherited from the 8-digit line if needed
|
|
39
|
+
special: str = ""
|
|
40
|
+
column2: str = ""
|
|
41
|
+
units: List[str] = field(default_factory=list)
|
|
42
|
+
declarable: bool = True # False for headings and subheadings that have more detailed lines under them
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return f"{self.code} {self.description} | general {self.general or '-'}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _digits(code) -> str:
|
|
49
|
+
return re.sub(r"\D", "", str(code))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_code(code) -> str:
|
|
53
|
+
"""'6201407000' -> '6201.40.70.00'."""
|
|
54
|
+
d = _digits(code)
|
|
55
|
+
parts = [d[:4], d[4:6], d[6:8], d[8:10]]
|
|
56
|
+
return ".".join(p for p in parts if p)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _path(i: int) -> List[list]:
|
|
60
|
+
stack: List[list] = []
|
|
61
|
+
start = i
|
|
62
|
+
while start > 0 and not (_ROWS[start][0] and len(_ROWS[start][0]) == 4):
|
|
63
|
+
start -= 1
|
|
64
|
+
for r in _ROWS[start:i + 1]:
|
|
65
|
+
while stack and stack[-1][1] >= r[1]:
|
|
66
|
+
stack.pop()
|
|
67
|
+
stack.append(r)
|
|
68
|
+
return stack
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _has_children(d: str) -> bool:
|
|
72
|
+
return any(k.startswith(d) and len(k) > len(d) for k in _INDEX)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def lookup(code) -> Optional[Line]:
|
|
76
|
+
"""Return the line for a 4- to 10-digit code, or None if it is not in the schedule.
|
|
77
|
+
|
|
78
|
+
A six- or eight-digit code that has no row of its own but has lines under it (common in the US
|
|
79
|
+
schedule) is returned as a non-declarable subheading with the description of its first line's parent.
|
|
80
|
+
"""
|
|
81
|
+
d = _digits(code)
|
|
82
|
+
i = _INDEX.get(d)
|
|
83
|
+
if i is None:
|
|
84
|
+
sub = sorted(k for k in _INDEX if k.startswith(d) and len(k) > len(d))
|
|
85
|
+
if len(d) < 4 or not sub:
|
|
86
|
+
return None
|
|
87
|
+
first = _INDEX[sub[0]]
|
|
88
|
+
path = _path(first)[:-1]
|
|
89
|
+
top = [k for k in sub if len(k) == min(len(x) for x in sub)]
|
|
90
|
+
if len(top) == 1 and len(top[0]) == 10:
|
|
91
|
+
# e.g. 6201.40.70 has only the line 6201.40.70.00: that line is the answer.
|
|
92
|
+
return lookup(top[0])
|
|
93
|
+
# A subheading with a single line under it carries its text on that line (e.g. 8471.30 -> 8471.30.01.00).
|
|
94
|
+
desc = _ROWS[first][2] if len(top) == 1 else (path[-1][2] if path else "")
|
|
95
|
+
return Line(format_code(d), desc, [(format_code(p[0]) if p[0] else "", p[2]) for p in path], "", declarable=False)
|
|
96
|
+
stack = _path(i)
|
|
97
|
+
r = _ROWS[i]
|
|
98
|
+
general = r[3] or next((s[3] for s in reversed(stack) if s[3]), "")
|
|
99
|
+
special = r[4] or next((s[4] for s in reversed(stack) if s[3]), "")
|
|
100
|
+
column2 = r[5] or next((s[5] for s in reversed(stack) if s[3]), "")
|
|
101
|
+
return Line(format_code(d), r[2], [(format_code(s[0]) if s[0] else "", s[2]) for s in stack], general, special, column2,
|
|
102
|
+
list(r[6]), declarable=not _has_children(d))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_valid(code) -> bool:
|
|
106
|
+
"""True if the code exists in the current US schedule (as a line or as a subheading with lines under it)."""
|
|
107
|
+
return lookup(code) is not None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def closest(code) -> Optional[Line]:
|
|
111
|
+
"""The longest valid code that the given code starts with, e.g. for a made-up 10-digit code."""
|
|
112
|
+
d = _digits(code)
|
|
113
|
+
for n in (10, 8, 6, 4):
|
|
114
|
+
if len(d) >= n:
|
|
115
|
+
hit = lookup(d[:n])
|
|
116
|
+
if hit:
|
|
117
|
+
return hit
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def children(code) -> List[Line]:
|
|
122
|
+
"""The next level of numbered lines under a heading or subheading."""
|
|
123
|
+
d = _digits(code)
|
|
124
|
+
kids = sorted(k for k in _INDEX if k.startswith(d) and len(k) > len(d))
|
|
125
|
+
if not kids:
|
|
126
|
+
return []
|
|
127
|
+
step = min(len(k) for k in kids)
|
|
128
|
+
return [lookup(k) for k in kids if len(k) == step]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def search(text: str, limit: int = 20, declarable_only: bool = True) -> List[Line]:
|
|
132
|
+
"""Lines whose full official path contains every word of `text` (case-insensitive)."""
|
|
133
|
+
words = [w for w in re.findall(r"[a-z0-9]+", text.lower()) if w]
|
|
134
|
+
out = []
|
|
135
|
+
for k, i in _INDEX.items():
|
|
136
|
+
if declarable_only and len(k) < 8:
|
|
137
|
+
continue
|
|
138
|
+
hay = " ".join(p[2] for p in _path(i)).lower()
|
|
139
|
+
if all(w in hay for w in words):
|
|
140
|
+
line = lookup(k)
|
|
141
|
+
if not declarable_only or line.declarable:
|
|
142
|
+
out.append(line)
|
|
143
|
+
if len(out) >= limit:
|
|
144
|
+
break
|
|
145
|
+
return out
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Command line: hts-lookup 6201.40.70 | hts-lookup --search "lithium ion" | hts-lookup --children 8507"""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from . import TITLE, children, closest, lookup, search
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def show(line):
|
|
9
|
+
rows = list(line.path) if line.declarable else list(line.path) + [(line.code, line.description)]
|
|
10
|
+
for i, (code, desc) in enumerate(rows):
|
|
11
|
+
print(f"{' ' * i}{code or '·'} {desc}")
|
|
12
|
+
if line.general:
|
|
13
|
+
print(f"General rate: {line.general}" + (f" | Special: {line.special}" if line.special else ""))
|
|
14
|
+
if not line.declarable:
|
|
15
|
+
print("Not a declarable line: more detailed codes exist under it (use --children).")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv=None):
|
|
19
|
+
p = argparse.ArgumentParser(prog="hts-lookup", description=f"US Harmonized Tariff Schedule lookup ({TITLE}).")
|
|
20
|
+
p.add_argument("code", nargs="?", help="HTS code, 4 to 10 digits, dots optional")
|
|
21
|
+
p.add_argument("--search", help="find lines whose official text contains all these words")
|
|
22
|
+
p.add_argument("--children", metavar="CODE", help="list the next level of lines under a code")
|
|
23
|
+
a = p.parse_args(argv)
|
|
24
|
+
if a.search:
|
|
25
|
+
for line in search(a.search):
|
|
26
|
+
print(line)
|
|
27
|
+
elif a.children:
|
|
28
|
+
for line in children(a.children):
|
|
29
|
+
print(line)
|
|
30
|
+
elif a.code:
|
|
31
|
+
line = lookup(a.code)
|
|
32
|
+
if line:
|
|
33
|
+
show(line)
|
|
34
|
+
else:
|
|
35
|
+
near = closest(a.code)
|
|
36
|
+
print(f"{a.code} is not in the US tariff schedule ({TITLE}).")
|
|
37
|
+
if near:
|
|
38
|
+
print(f"Closest valid code: {near.code} {near.description}")
|
|
39
|
+
return 1
|
|
40
|
+
else:
|
|
41
|
+
p.print_help()
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
sys.exit(main())
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: hts-lookup
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes: official descriptions, duty rates, closest valid code. No dependencies.
|
|
5
|
+
Author: Anders Lee
|
|
6
|
+
License: MIT (code); public domain US government data (see DATA-LICENSE.md)
|
|
7
|
+
Project-URL: Homepage, https://hs-code.net/
|
|
8
|
+
Keywords: hts,hs code,harmonized tariff schedule,tariff,customs,duty rate,import,trade,classification,usitc
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Office/Business
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# hts-lookup
|
|
19
|
+
|
|
20
|
+
Offline lookup and validation of US Harmonized Tariff Schedule (HTS) codes. The current schedule is bundled
|
|
21
|
+
(about 0.5 MB compressed), so it works without network access or API keys. Pure Python, no dependencies.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install hts-lookup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import hts_lookup as hts
|
|
29
|
+
|
|
30
|
+
hts.TITLE # 'Revision 21 (2026)'
|
|
31
|
+
line = hts.lookup("6201.40.70")
|
|
32
|
+
line.description # 'Water resistant (634)'
|
|
33
|
+
line.general # '7.1%'
|
|
34
|
+
[c for c, _ in line.path] # ['6201', '6201.40', '', '', '', '', '', '6201.40.70.00']
|
|
35
|
+
|
|
36
|
+
hts.lookup("6109.10.00.12").general # '16.5%' (10-digit lines inherit the 8-digit rate)
|
|
37
|
+
hts.lookup("6201.40").declarable # False (a subheading with lines under it)
|
|
38
|
+
hts.lookup("8471.30").code # '8471.30.01.00' (only one line under it, so that line)
|
|
39
|
+
|
|
40
|
+
hts.is_valid("6201.40.99.99") # False
|
|
41
|
+
hts.closest("6201409999").code # '6201.40'
|
|
42
|
+
|
|
43
|
+
[l.code for l in hts.children("8507.60")]
|
|
44
|
+
hts.search("lithium ion", limit=5)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Command line:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
$ hts-lookup 6201.40.70
|
|
51
|
+
$ hts-lookup --search "vinyl stickers"
|
|
52
|
+
$ hts-lookup --children 4202.92
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why validate codes
|
|
56
|
+
|
|
57
|
+
HS and HTS codes copied from supplier invoices, old spreadsheets or AI chat answers are often out of date or do not exist.
|
|
58
|
+
`is_valid()` and `closest()` catch those before a shipment does.
|
|
59
|
+
|
|
60
|
+
## Notes
|
|
61
|
+
|
|
62
|
+
- `general` is the column 1 general (MFN) rate. Extra duties under Chapter 99 depend on the country of origin and are not applied.
|
|
63
|
+
- The first six digits are the international HS code; digits 7 to 10 are US-specific.
|
|
64
|
+
- The bundled schedule is updated with new USITC revisions; check `hts_lookup.TITLE` for the edition you have.
|
|
65
|
+
|
|
66
|
+
Step-by-step product guides, UK and EU rates, and the customs rulings behind each code: [hs-code.net](https://hs-code.net/)
|
|
67
|
+
|
|
68
|
+
Code: MIT. Data: public domain US government work, see DATA-LICENSE.md. Not affiliated with USITC or CBP.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/hts_lookup/__init__.py
|
|
5
|
+
src/hts_lookup/__main__.py
|
|
6
|
+
src/hts_lookup.egg-info/PKG-INFO
|
|
7
|
+
src/hts_lookup.egg-info/SOURCES.txt
|
|
8
|
+
src/hts_lookup.egg-info/dependency_links.txt
|
|
9
|
+
src/hts_lookup.egg-info/entry_points.txt
|
|
10
|
+
src/hts_lookup.egg-info/top_level.txt
|
|
11
|
+
src/hts_lookup/data/hts.json.gz
|
|
12
|
+
tests/test_hts_lookup.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hts_lookup
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import hts_lookup as hts
|
|
2
|
+
from hts_lookup.__main__ import main
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_full_line_and_inherited_rate():
|
|
6
|
+
line = hts.lookup("6201.40.70.00")
|
|
7
|
+
assert line.description.startswith("Water resistant")
|
|
8
|
+
assert line.general == "7.1%"
|
|
9
|
+
assert line.declarable
|
|
10
|
+
assert line.path[0][0] == "6201"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_stat_suffix_inherits_8_digit_rate():
|
|
14
|
+
assert hts.lookup("6109.10.00.12").general == "16.5%"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_subheading_with_one_line_returns_that_line():
|
|
18
|
+
line = hts.lookup("8471.30")
|
|
19
|
+
assert line.code == "8471.30.01.00" and line.declarable and line.general == "Free"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_subheading_with_several_lines():
|
|
23
|
+
line = hts.lookup("6201.40")
|
|
24
|
+
assert line is not None and not line.declarable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_invalid_and_closest():
|
|
28
|
+
assert not hts.is_valid("6201.40.99.99")
|
|
29
|
+
assert hts.closest("6201409999").code == "6201.40"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_children_and_search():
|
|
33
|
+
kids = [l.code for l in hts.children("8507.60")]
|
|
34
|
+
assert "8507.60.00" in kids or "8507.60.00.10" in kids
|
|
35
|
+
assert any(l.code.startswith("8507.60") for l in hts.search("lithium ion batteries"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_format_and_cli(capsys):
|
|
39
|
+
assert hts.format_code("6201407000") == "6201.40.70.00"
|
|
40
|
+
assert main(["6201.40.70"]) == 0
|
|
41
|
+
assert "Water resistant" in capsys.readouterr().out
|
|
42
|
+
assert main(["9999.99.99"]) == 1
|