pyhscodes 1.0.0__py3-none-any.whl
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.
- pyhscodes/COPYRIGHT.txt +24 -0
- pyhscodes/__init__.py +128 -0
- pyhscodes/databases/hscodes.json +48584 -0
- pyhscodes/databases/sections.json +88 -0
- pyhscodes/db.py +217 -0
- pyhscodes/py.typed +0 -0
- pyhscodes/tests/test_general.py +282 -0
- pyhscodes-1.0.0.dist-info/LICENSE.txt +458 -0
- pyhscodes-1.0.0.dist-info/METADATA +179 -0
- pyhscodes-1.0.0.dist-info/RECORD +11 -0
- pyhscodes-1.0.0.dist-info/WHEEL +4 -0
pyhscodes/COPYRIGHT.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
COPYRIGHT (c) 2008 - 2023, pycountry
|
|
2
|
+
|
|
3
|
+
Pycountry is free software; you can redistribute it and/or modify
|
|
4
|
+
it under the terms of the GNU Lesser General Public License as published by
|
|
5
|
+
the Free Software Foundation; either version 2.1 of the License, or any later version.
|
|
6
|
+
|
|
7
|
+
This project is distributed in the hope that it will be useful,
|
|
8
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
9
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
10
|
+
GNU Lesser General Public License for more details.
|
|
11
|
+
|
|
12
|
+
Contributors:
|
|
13
|
+
- See Pycountry contributors until 2025
|
|
14
|
+
- David Sigley (2025)
|
|
15
|
+
|
|
16
|
+
Additional Acknowledgements and Licensing Information:
|
|
17
|
+
- Data in the /src/databases/ and /src/locales/ folders is sourced from the Debian iso-codes project, available at: https://salsa.debian.org/iso-codes-team/iso-codes. This data is used under the terms of the GNU Lesser General Public License Version 2.1 (February 1999).
|
|
18
|
+
|
|
19
|
+
The Debian iso-codes project is a collection of code lists for different standards, maintained and made available under the GNU Lesser General Public License Version 2.1. We gratefully acknowledge the Debian iso-codes team and contributors for their work and for making this resource freely available.
|
|
20
|
+
|
|
21
|
+
The full text of the GNU Lesser General Public License Version 2.1 can be found at: https://salsa.debian.org/iso-codes-team/iso-codes/-/blob/main/COPYING.
|
|
22
|
+
|
|
23
|
+
For the full text of the GNU Lesser General Public License,
|
|
24
|
+
see https://github.com/pycountry/pycountry/blob/main/LICENSE.txt.
|
pyhscodes/__init__.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""pyhscodes - Harmonized System codes from the World Customs Organization"""
|
|
2
|
+
|
|
3
|
+
import os.path
|
|
4
|
+
import unicodedata
|
|
5
|
+
from importlib import metadata as _importlib_metadata
|
|
6
|
+
from typing import Dict, List, Optional, Type
|
|
7
|
+
|
|
8
|
+
import pyhscodes.db
|
|
9
|
+
|
|
10
|
+
# We prioritise importing the backported `importlib_resources`
|
|
11
|
+
# because the function we use (`importlib.resources.files`) is only
|
|
12
|
+
# available from Python 3.9, but the module itself exists since 3.7.
|
|
13
|
+
# We install `importlib_resources` on Python < 3.9.
|
|
14
|
+
# TODO: Remove usage of importlib_resources once support for Python 3.8 is dropped
|
|
15
|
+
try:
|
|
16
|
+
import importlib_resources # type: ignore
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
from importlib import resources as importlib_resources # type: ignore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resource_filename(package_or_requirement: str, resource_name: str) -> str:
|
|
22
|
+
return str(importlib_resources.files(package_or_requirement) / resource_name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_version(distribution_name: str) -> Optional[str]:
|
|
26
|
+
try:
|
|
27
|
+
return _importlib_metadata.version(distribution_name)
|
|
28
|
+
except _importlib_metadata.PackageNotFoundError:
|
|
29
|
+
return "n/a"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Variable annotations
|
|
33
|
+
DATABASE_DIR: str = resource_filename("pyhscodes", "databases")
|
|
34
|
+
__version__: Optional[str] = get_version("pyhscodes")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def remove_accents(input_str: str) -> str:
|
|
38
|
+
output_str = input_str
|
|
39
|
+
if not input_str.isascii():
|
|
40
|
+
# Borrowed from https://stackoverflow.com/a/517974/1509718
|
|
41
|
+
nfkd_form = unicodedata.normalize("NFKD", input_str)
|
|
42
|
+
output_str = "".join([c for c in nfkd_form if not unicodedata.combining(c)])
|
|
43
|
+
return output_str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HSCodes(pyhscodes.db.Database):
|
|
47
|
+
"""Provides access to Harmonized System codes database."""
|
|
48
|
+
|
|
49
|
+
data_class = pyhscodes.db.HSCode
|
|
50
|
+
root_key = "hscodes"
|
|
51
|
+
|
|
52
|
+
def search_fuzzy(self, query: str) -> List[Type["HSCodes"]]:
|
|
53
|
+
query = remove_accents(query.strip().lower())
|
|
54
|
+
|
|
55
|
+
# A code to points mapping for later sorting codes
|
|
56
|
+
# based on the query's matching incidence.
|
|
57
|
+
results: dict[str, int] = {}
|
|
58
|
+
|
|
59
|
+
def add_result(hscode: "pyhscodes.db.HSCode", points: int) -> None:
|
|
60
|
+
results.setdefault(hscode.hscode, 0)
|
|
61
|
+
results[hscode.hscode] += points
|
|
62
|
+
|
|
63
|
+
# Prio 1: exact matches on HS code numbers
|
|
64
|
+
try:
|
|
65
|
+
add_result(self.lookup(query), 50)
|
|
66
|
+
except LookupError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# Prio 2: exact matches on descriptions
|
|
70
|
+
try:
|
|
71
|
+
result = self.get(description=query)
|
|
72
|
+
if result is not None:
|
|
73
|
+
add_result(result, 49)
|
|
74
|
+
except (LookupError, TypeError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# Prio 3: partial matches on descriptions
|
|
78
|
+
for candidate in self:
|
|
79
|
+
description = candidate._fields.get("description")
|
|
80
|
+
if description is not None:
|
|
81
|
+
description = remove_accents(description.lower())
|
|
82
|
+
if query in description:
|
|
83
|
+
# This prefers codes with a match early in their description
|
|
84
|
+
add_result(candidate, max([5, 30 - (2 * description.find(query))]))
|
|
85
|
+
|
|
86
|
+
if not results:
|
|
87
|
+
raise LookupError(query)
|
|
88
|
+
|
|
89
|
+
sorted_results = [
|
|
90
|
+
self.get(hscode=x[0])
|
|
91
|
+
# sort by points first, by hscode second
|
|
92
|
+
for x in sorted(results.items(), key=lambda x: (-x[1], x[0]))
|
|
93
|
+
]
|
|
94
|
+
return sorted_results
|
|
95
|
+
|
|
96
|
+
def get_by_level(self, level: str) -> List[pyhscodes.db.HSCode]:
|
|
97
|
+
"""Get all HS codes at a specific level (2, 4, or 6)."""
|
|
98
|
+
return [obj for obj in self if obj.level == level]
|
|
99
|
+
|
|
100
|
+
def get_children(self, parent_code: str) -> List[pyhscodes.db.HSCode]:
|
|
101
|
+
"""Get all child codes for a given parent code."""
|
|
102
|
+
return [obj for obj in self if obj.parent == parent_code]
|
|
103
|
+
|
|
104
|
+
def get_hierarchy(self, hscode: str) -> List[pyhscodes.db.HSCode]:
|
|
105
|
+
"""Get the full hierarchy path for a given HS code."""
|
|
106
|
+
hierarchy = []
|
|
107
|
+
current = self.get(hscode=hscode)
|
|
108
|
+
|
|
109
|
+
while current:
|
|
110
|
+
hierarchy.insert(0, current)
|
|
111
|
+
if current.parent and current.parent != "TOTAL":
|
|
112
|
+
current = self.get(hscode=current.parent)
|
|
113
|
+
else:
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
return hierarchy
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Sections(pyhscodes.db.Database):
|
|
120
|
+
"""Provides access to HS code sections database."""
|
|
121
|
+
|
|
122
|
+
data_class = pyhscodes.db.Section
|
|
123
|
+
root_key = "sections"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Create database instances
|
|
127
|
+
hscodes = HSCodes(os.path.join(DATABASE_DIR, "hscodes.json"))
|
|
128
|
+
sections = Sections(os.path.join(DATABASE_DIR, "sections.json"))
|