zipcodes 2.0.1__cp39-abi3-win_amd64.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.
zipcodes/__init__.py ADDED
@@ -0,0 +1,174 @@
1
+ """
2
+ zipcodes
3
+ ~~~~~~~~
4
+
5
+ No-SQLite U.S. zipcode validation Python package, ready for use in AWS Lambda
6
+
7
+ :author: Sean Pianka
8
+ :github: @seanpianka
9
+
10
+ The full-database scans run in the compiled Rust extension
11
+ (``zipcodes._zipcodes``); this module preserves the exact 1.x behavior for
12
+ argument validation, exceptions, and the ``zips=`` chaining lists.
13
+ """
14
+ import re
15
+ import warnings
16
+ from math import asin, cos, radians, sin, sqrt
17
+
18
+ from zipcodes import _zipcodes
19
+
20
+ __author__ = "Sean Pianka"
21
+ __email__ = "pianka@eml.cc"
22
+ __license__ = "MIT"
23
+ __version__ = _zipcodes.__version__
24
+
25
+ _digits = re.compile(r"[^\d\-]")
26
+ _valid_zipcode_length = 5
27
+
28
+ _zips_cache = None
29
+
30
+
31
+ def _load_zips():
32
+ global _zips_cache
33
+ if _zips_cache is None:
34
+ _zips_cache = _zipcodes.list_all()
35
+ return _zips_cache
36
+
37
+
38
+ def __getattr__(name):
39
+ # `_zips` was a module-level list in 1.x; keep it importable, but
40
+ # materialize the 42k dicts lazily instead of at import time.
41
+ if name == "_zips":
42
+ return _load_zips()
43
+ raise AttributeError("module {!r} has no attribute {!r}".format(__name__, name))
44
+
45
+
46
+ def _clean_zipcode(fn):
47
+ def decorator(zipcode, *args, **kwargs):
48
+ if not zipcode or not isinstance(zipcode, str):
49
+ raise TypeError("Invalid type, zipcode must be a string.")
50
+
51
+ return fn(
52
+ _clean(zipcode, min(len(zipcode), _valid_zipcode_length)), *args, **kwargs
53
+ )
54
+
55
+ return decorator
56
+
57
+
58
+ @_clean_zipcode
59
+ def matching(zipcode, zips=None):
60
+ """Retrieve zipcode dict for provided zipcode"""
61
+ if zips is None:
62
+ return _zipcodes.matching(zipcode)
63
+ return [z for z in zips if z["zip_code"] == zipcode]
64
+
65
+
66
+ @_clean_zipcode
67
+ def is_valid(zipcode):
68
+ warnings.warn("is_valid is deprecated; use is_real", DeprecationWarning, stacklevel=2)
69
+ return is_real(zipcode)
70
+
71
+
72
+ @_clean_zipcode
73
+ def is_real(zipcode):
74
+ """Determine whether a given zip or zip+4 zipcode is real."""
75
+ return _zipcodes.is_real(zipcode)
76
+
77
+
78
+ @_clean_zipcode
79
+ def similar_to(partial_zipcode, zips=None):
80
+ """List of zipcode dicts where zipcode prefix matches `partial_zipcode`"""
81
+ if zips is None:
82
+ return _zipcodes.similar_to(partial_zipcode)
83
+ return [z for z in zips if z["zip_code"].startswith(partial_zipcode)]
84
+
85
+
86
+ @_clean_zipcode
87
+ def contains(partial_zipcode, zips=None):
88
+ """List of zipcode dicts where zipcode contains `partial_zipcode` fragment"""
89
+ if zips is None:
90
+ return _zipcodes.contains(partial_zipcode)
91
+ return [z for z in zips if partial_zipcode in z["zip_code"]]
92
+
93
+
94
+ def filter_by_state(state, zips=None):
95
+ return filter_by(zips, state=state)
96
+
97
+
98
+ def filter_by_city(city, zips=None):
99
+ return filter_by(zips, city=city)
100
+
101
+
102
+ def filter_by_county(county, zips=None):
103
+ return filter_by(zips, county=county)
104
+
105
+
106
+ def filter_by_timezone(timezone, zips=None):
107
+ return filter_by(zips, timezone=timezone)
108
+
109
+
110
+ def filter_by_zip_code_type(zip_code_type, zips=None):
111
+ return filter_by(zips, zip_code_type=zip_code_type)
112
+
113
+
114
+ def haversine(lon1, lat1, lon2, lat2):
115
+ """
116
+ Calculate the great circle distance in miles between two points
117
+ on the earth (specified in decimal degrees)
118
+ """
119
+ lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
120
+
121
+ dlon = lon2 - lon1
122
+ dlat = lat2 - lat1
123
+ a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
124
+ c = 2 * asin(sqrt(a))
125
+ r = 3956 # Radius of earth in miles. Use 6371 for kilometers.
126
+ return c * r
127
+
128
+
129
+ def filter_by_coordinates(lat, long, radius_in_miles=10, zips=None):
130
+ """List of zipcode dicts within `radius_in_miles` of (`lat`, `long`)."""
131
+ if zips is None:
132
+ return _zipcodes.filter_by_coordinates(lat, long, radius_in_miles)
133
+ return [
134
+ z
135
+ for z in zips
136
+ if haversine(float(z["long"]), float(z["lat"]), long, lat) <= radius_in_miles
137
+ ]
138
+
139
+
140
+ def filter_by(zips=None, **filters):
141
+ """Use `kwargs` to select for desired attributes from list of zipcode dicts"""
142
+ if zips is None:
143
+ return _zipcodes.filter_by(**filters)
144
+ return [
145
+ z
146
+ for z in zips
147
+ if all(key in z and z[key] == value for key, value in filters.items())
148
+ ]
149
+
150
+
151
+ def list_all(zips=None):
152
+ """Return a list containing all zip-code objects."""
153
+ if zips is None:
154
+ return _load_zips()
155
+ return zips
156
+
157
+
158
+ def _contains_nondigits(s):
159
+ return bool(_digits.search(s))
160
+
161
+
162
+ def _clean(zipcode, valid_length=_valid_zipcode_length):
163
+ """Assumes zipcode is of type `str`"""
164
+ zipcode = zipcode.split("-")[0] # Convert #####-#### to #####
165
+
166
+ if len(zipcode) != valid_length:
167
+ raise ValueError(
168
+ 'Invalid format, zipcode must be of the format: "#####" or "#####-####"'
169
+ )
170
+
171
+ if _contains_nondigits(zipcode):
172
+ raise ValueError('Invalid characters, zipcode may only contain digits and "-".')
173
+
174
+ return zipcode
zipcodes/_zipcodes.pyd ADDED
Binary file
zipcodes/_zipcodes.pyi ADDED
@@ -0,0 +1,14 @@
1
+ from typing import Any, Dict, List
2
+
3
+ __version__: str
4
+
5
+ def matching(zipcode: str) -> List[Dict[str, Any]]: ...
6
+ def is_real(zipcode: str) -> bool: ...
7
+ def similar_to(prefix: str) -> List[Dict[str, Any]]: ...
8
+ def contains(fragment: str) -> List[Dict[str, Any]]: ...
9
+ def filter_by(**filters: Any) -> List[Dict[str, Any]]: ...
10
+ def filter_by_coordinates(
11
+ lat: float, long: float, radius_in_miles: float
12
+ ) -> List[Dict[str, Any]]: ...
13
+ def haversine(lon1: float, lat1: float, lon2: float, lat2: float) -> float: ...
14
+ def list_all() -> List[Dict[str, Any]]: ...
zipcodes/py.typed ADDED
File without changes
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: zipcodes
3
+ Version: 2.0.1
4
+ Classifier: Development Status :: 5 - Production/Stable
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Programming Language :: Python :: 3.9
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Programming Language :: Rust
13
+ License-File: LICENSE.txt
14
+ Summary: Query U.S. state zipcodes without SQLite.
15
+ Keywords: zipcode,zip,code,us,state,query,filter,validate,sqlite
16
+ Home-Page: https://github.com/seanpianka/zipcodes
17
+ Author-email: Sean Pianka <pianka@eml.cc>
18
+ License-Expression: MIT
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
+ Project-URL: Homepage, https://github.com/seanpianka/zipcodes
22
+ Project-URL: Issues, https://github.com/seanpianka/zipcodes/issues
23
+
24
+ # Zipcodes
25
+
26
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zipcodes)
27
+ [![PyPI](https://img.shields.io/pypi/v/zipcodes)](https://pypi.org/project/zipcodes/)
28
+ [![crates.io](https://img.shields.io/crates/v/zipcodes)](https://crates.io/crates/zipcodes)
29
+ [![Downloads](https://static.pepy.tech/badge/zipcodes/month)](https://pepy.tech/project/zipcodes)
30
+ [![Contributors](https://img.shields.io/github/contributors/seanpianka/zipcodes.svg)](https://github.com/seanpianka/zipcodes/graphs/contributors)
31
+
32
+ Zipcodes is a simple library for querying U.S. zipcodes. No SQLite, no
33
+ network, no runtime data files — the full dataset is embedded in the package.
34
+
35
+ Since 2.0, the library is implemented in Rust and published from a single
36
+ codebase as both the [`zipcodes` Python package](https://pypi.org/project/zipcodes/)
37
+ and the [`zipcodes` Rust crate](https://crates.io/crates/zipcodes). The Python
38
+ API is a drop-in replacement for 1.x — same functions, same dicts, same
39
+ exceptions — just faster:
40
+
41
+ | Operation | 1.x (pure Python) | 2.0 (Rust) |
42
+ |---|---|---|
43
+ | `import zipcodes` | ~330 ms (loads dataset) | ~5 ms (dataset loads lazily on first query, ~200 ms) |
44
+ | `is_real("06903")` | ~4.2 ms | ~0.03 ms |
45
+ | `matching("77429")` | ~4.2 ms | ~0.3 ms |
46
+ | `similar_to("1018")` | ~7.2 ms | ~0.3 ms |
47
+ | `filter_by(state="TX")` | ~9.7 ms | ~3.7 ms |
48
+
49
+ ```python
50
+ >>> import zipcodes
51
+ >>> assert zipcodes.is_real('77429')
52
+ >>> assert len(zipcodes.similar_to('7742')) != 0
53
+ >>> exact_zip = zipcodes.matching('77429')[0]
54
+ >>> filtered_zips = zipcodes.filter_by(city="Cypress", state="TX")
55
+ >>> assert exact_zip in filtered_zips
56
+ >>> pprint.pprint(exact_zip)
57
+ {'acceptable_cities': [],
58
+ 'active': True,
59
+ 'area_codes': ['281', '346', '713', '832'],
60
+ 'city': 'Cypress',
61
+ 'country': 'US',
62
+ 'county': 'Harris County',
63
+ 'lat': '29.9766',
64
+ 'long': '-95.6358',
65
+ 'state': 'TX',
66
+ 'timezone': 'America/Chicago',
67
+ 'unacceptable_cities': [],
68
+ 'world_region': 'NA',
69
+ 'zip_code': '77429',
70
+ 'zip_code_type': 'STANDARD'}
71
+ ```
72
+
73
+ The zipcode data is refreshed automatically every month — see
74
+ [Zipcode Data](#zipcode-data).
75
+
76
+ ## Installation
77
+
78
+ ### Python
79
+
80
+ ```console
81
+ $ python -m pip install zipcodes
82
+ ```
83
+
84
+ Zipcodes 2.x supports Python 3.9+ and ships prebuilt wheels for Linux
85
+ (x86_64, aarch64, musl), macOS, and Windows. Installing from the source
86
+ distribution requires a Rust toolchain. Python 2.6+/3.2+ users are
87
+ automatically served the pure-Python 1.3.0 release by pip.
88
+
89
+ ### Rust
90
+
91
+ ```console
92
+ $ cargo add zipcodes
93
+ ```
94
+
95
+ ### New in 2.0
96
+
97
+ - Implemented in Rust; the dataset is compiled into the extension module and
98
+ decompressed lazily on first query, so `import zipcodes` is effectively free.
99
+ - New functions: `contains`, `filter_by_state`, `filter_by_city`,
100
+ `filter_by_county`, `filter_by_timezone`, `filter_by_zip_code_type`,
101
+ `filter_by_coordinates`, and `haversine`.
102
+ - `is_valid` now actually emits its `DeprecationWarning` (in 1.x it raised
103
+ `AttributeError`); use `is_real`.
104
+ - PyInstaller users no longer need `--add-data` for `zips.json.bz2` — there is
105
+ no data file anymore.
106
+ - Behavioral notes for upgraders: query results are fresh dicts (mutating a
107
+ result no longer mutates the shared database list), and
108
+ `filter_by(active=1)` no longer matches `active=True` (pass a bool).
109
+
110
+ ## Zipcode Data
111
+
112
+ The embedded dataset (`crates/zipcodes/src/zips.json.bz2`) is assembled from
113
+ three sources:
114
+
115
+ - **[unitedstateszipcodes.org](https://www.unitedstateszipcodes.org)** — the
116
+ rich base data (city aliases, zip type, area codes, county, timezone),
117
+ committed in-repo as `scripts/data/zip_code_database.csv`. Its bot
118
+ protection prevents automated downloads, so this file is refreshed manually
119
+ on occasion.
120
+ - **[GeoNames](https://www.geonames.org/)** (`download.geonames.org/export/zip/US.zip`) —
121
+ GPS coordinates, fetched fresh on every update. Licensed
122
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
123
+ - **USPS ZIP Locale Detail** ([postalpro.usps.com](https://postalpro.usps.com/ZIP_Locale_Detail)) —
124
+ the authoritative list of active delivery ZIPs, fetched fresh on every
125
+ update and used to add zipcodes missing from the base CSV
126
+ ([#23](https://github.com/seanpianka/Zipcodes/issues/23)). Public domain.
127
+
128
+ A GitHub Actions workflow ([`update-data.yml`](.github/workflows/update-data.yml))
129
+ rebuilds the dataset on the 1st of every month
130
+ ([#7](https://github.com/seanpianka/Zipcodes/issues/7)). If anything changed,
131
+ it opens a pull request with a summary of added/removed/modified records;
132
+ merging that PR tags a patch release, which publishes to PyPI and crates.io
133
+ automatically.
134
+
135
+ To rebuild the dataset manually:
136
+
137
+ ```shell script
138
+ $ pip install xlrd
139
+ $ curl -fsSL https://download.geonames.org/export/zip/US.zip -o /tmp/geonames_us.zip
140
+ $ # download the .xls linked from https://postalpro.usps.com/ZIP_Locale_Detail
141
+ $ python scripts/update_zipcode_dataset.py \
142
+ --base scripts/data/zip_code_database.csv \
143
+ --gps scripts/data/zip-codes-database-FREE.csv \
144
+ --geonames-zip /tmp/geonames_us.zip \
145
+ --usps-xls /tmp/usps_zip_locale.xls \
146
+ --output-bz2 crates/zipcodes/src/zips.json.bz2 \
147
+ --summary-output /tmp/change_summary.json
148
+ ```
149
+
150
+ ## Tests
151
+
152
+ The tests are defined in a declarative, table-based format that generates test
153
+ cases.
154
+
155
+ ```shell script
156
+ $ cargo test # Rust unit tests
157
+ $ python tests/__init__.py # Python suite (or: pytest tests/)
158
+ ```
159
+
160
+ ## Examples
161
+
162
+ ```python
163
+ >>> from pprint import pprint
164
+ >>> import zipcodes
165
+
166
+ >>> # Simple zip-code matching.
167
+ >>> pprint(zipcodes.matching('77429'))
168
+ [{'acceptable_cities': [],
169
+ 'active': True,
170
+ 'area_codes': ['281', '346', '713', '832'],
171
+ 'city': 'Cypress',
172
+ 'country': 'US',
173
+ 'county': 'Harris County',
174
+ 'lat': '29.9766',
175
+ 'long': '-95.6358',
176
+ 'state': 'TX',
177
+ 'timezone': 'America/Chicago',
178
+ 'unacceptable_cities': [],
179
+ 'world_region': 'NA',
180
+ 'zip_code': '77429',
181
+ 'zip_code_type': 'STANDARD'}]
182
+
183
+
184
+ >>> # Handles Zip+4 zip-codes nicely. :)
185
+ >>> pprint(zipcodes.matching('77429-1145'))
186
+ [{'acceptable_cities': [],
187
+ 'active': True,
188
+ 'area_codes': ['281', '346', '713', '832'],
189
+ 'city': 'Cypress',
190
+ 'country': 'US',
191
+ 'county': 'Harris County',
192
+ 'lat': '29.9766',
193
+ 'long': '-95.6358',
194
+ 'state': 'TX',
195
+ 'timezone': 'America/Chicago',
196
+ 'unacceptable_cities': [],
197
+ 'world_region': 'NA',
198
+ 'zip_code': '77429',
199
+ 'zip_code_type': 'STANDARD'}]
200
+
201
+ >>> # Will try to handle invalid zip-codes gracefully...
202
+ >>> print(zipcodes.matching('06463'))
203
+ []
204
+
205
+ >>> # Until it cannot.
206
+ >>> zipcodes.matching('0646a')
207
+ Traceback (most recent call last):
208
+ ...
209
+ ValueError: Invalid characters, zipcode may only contain digits and "-".
210
+
211
+ >>> zipcodes.matching('064690')
212
+ Traceback (most recent call last):
213
+ ...
214
+ ValueError: Invalid format, zipcode must be of the format: "#####" or "#####-####"
215
+
216
+ >>> zipcodes.matching(None)
217
+ Traceback (most recent call last):
218
+ ...
219
+ TypeError: Invalid type, zipcode must be a string.
220
+
221
+ >>> # Whether the zip-code exists within the database.
222
+ >>> print(zipcodes.is_real('06463'))
223
+ False
224
+
225
+ >>> # How handy!
226
+ >>> print(zipcodes.is_real('06469'))
227
+ True
228
+
229
+ >>> # Search for zipcodes that begin with a pattern.
230
+ >>> pprint(zipcodes.similar_to('1018'))
231
+ [{'acceptable_cities': [],
232
+ 'active': False,
233
+ 'area_codes': ['212'],
234
+ 'city': 'New York',
235
+ 'country': 'US',
236
+ 'county': 'New York County',
237
+ 'lat': '40.71',
238
+ 'long': '-74',
239
+ 'state': 'NY',
240
+ 'timezone': 'America/New_York',
241
+ 'unacceptable_cities': ['J C Penney'],
242
+ 'world_region': 'NA',
243
+ 'zip_code': '10184',
244
+ 'zip_code_type': 'UNIQUE'},
245
+ {'acceptable_cities': [],
246
+ 'active': True,
247
+ 'area_codes': ['212'],
248
+ 'city': 'New York',
249
+ 'country': 'US',
250
+ 'county': 'New York County',
251
+ 'lat': '40.7808',
252
+ 'long': '-73.9772',
253
+ 'state': 'NY',
254
+ 'timezone': 'America/New_York',
255
+ 'unacceptable_cities': ['Manhattan', 'New York City', 'NY', 'Ny City', 'Nyc'],
256
+ 'world_region': 'NA',
257
+ 'zip_code': '10185',
258
+ 'zip_code_type': 'PO BOX'}]
259
+
260
+ >>> # Use filter_by to filter a list of zip-codes by specific attribute->value pairs.
261
+ >>> pprint(zipcodes.filter_by(city="Old Saybrook"))
262
+ [{'acceptable_cities': [],
263
+ 'active': True,
264
+ 'area_codes': ['860', '959'],
265
+ 'city': 'Old Saybrook',
266
+ 'country': 'US',
267
+ 'county': 'Middlesex County',
268
+ 'lat': '41.2913',
269
+ 'long': '-72.385',
270
+ 'state': 'CT',
271
+ 'timezone': 'America/New_York',
272
+ 'unacceptable_cities': ['Fenwick'],
273
+ 'world_region': 'NA',
274
+ 'zip_code': '06475',
275
+ 'zip_code_type': 'STANDARD'}]
276
+
277
+ >>> # Arbitrary nesting of similar_to and filter_by calls, allowing for great precision while filtering.
278
+ >>> pprint([z['zip_code'] for z in zipcodes.similar_to('2', zips=zipcodes.filter_by(active=True, city='Windsor'))])
279
+ ['23487', '27983', '29856']
280
+
281
+ >>> # Find zipcodes within a radius (miles) of a coordinate.
282
+ >>> pprint([z['zip_code'] for z in zipcodes.filter_by_coordinates(41.3015, -72.3879, 5)])
283
+ ['06371', '06409', '06426', '06442', '06475', '06498']
284
+
285
+ >>> # Have any other ideas? Make a pull request and start contributing today!
286
+ >>> # Made with love by Sean Pianka
287
+ ```
288
+
289
+ ## Repository layout
290
+
291
+ This repository builds both packages from one Rust core:
292
+
293
+ - `crates/zipcodes` — the core library, published to
294
+ [crates.io](https://crates.io/crates/zipcodes).
295
+ - `crates/zipcodes-py` — PyO3 bindings (not published to crates.io).
296
+ - `python/zipcodes` — the thin Python compatibility layer; together with the
297
+ bindings it forms the [PyPI package](https://pypi.org/project/zipcodes/),
298
+ built with [maturin](https://github.com/PyO3/maturin).
299
+
@@ -0,0 +1,9 @@
1
+ zipcodes/__init__.py,sha256=SrMF4erWLz7vwWgaqpz1TGN8w6LxxLMiXtwIwZp4KWw,4999
2
+ zipcodes/_zipcodes.pyd,sha256=gCcs7IwV4dXwphaO4a__kiozYiEkGBaZn4DSLa3simw,1109504
3
+ zipcodes/_zipcodes.pyi,sha256=0ycFzEWpGs-EzUMiG-bi9tVA71G3AR-RwVh6quSiuMk,570
4
+ zipcodes/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ zipcodes-2.0.1.dist-info/METADATA,sha256=ScTWJtLprfmg-JFPeCgGsS_ASvBBhgMwW6T-p546s8I,10675
6
+ zipcodes-2.0.1.dist-info/WHEEL,sha256=ZiBmjEMKX1iTyJmqXZl_jTcrFdOtPkEbr7SVi1lbRvI,95
7
+ zipcodes-2.0.1.dist-info/licenses/LICENSE.txt,sha256=vC0EMImFh3XwWiWttQX4zGDytVodWMDmpf4Fk0-GLR4,1061
8
+ zipcodes-2.0.1.dist-info/sboms/zipcodes-py.cyclonedx.json,sha256=11WrzD1EqzXpTc4AWQScxQixXyfuDKs_83KD7jwR8tI,30638
9
+ zipcodes-2.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.14.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64
@@ -0,0 +1,20 @@
1
+ The MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+