zipcodes 2.0.1__tar.gz → 3.0.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.
- {zipcodes-2.0.1 → zipcodes-3.0.0}/Cargo.lock +2 -2
- {zipcodes-2.0.1 → zipcodes-3.0.0}/Cargo.toml +1 -1
- {zipcodes-2.0.1 → zipcodes-3.0.0}/PKG-INFO +13 -2
- {zipcodes-2.0.1 → zipcodes-3.0.0}/README.md +12 -1
- zipcodes-3.0.0/crates/zipcodes/examples/gen_schema.rs +12 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/crates/zipcodes/src/lib.rs +98 -1
- zipcodes-3.0.0/crates/zipcodes-py/src/lib.rs +261 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/python/zipcodes/__init__.py +20 -73
- zipcodes-3.0.0/python/zipcodes/_zipcodes.pyi +26 -0
- zipcodes-2.0.1/crates/zipcodes-py/src/lib.rs +0 -156
- zipcodes-2.0.1/python/zipcodes/_zipcodes.pyi +0 -14
- {zipcodes-2.0.1 → zipcodes-3.0.0}/LICENSE.txt +0 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/crates/zipcodes/Cargo.toml +0 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/crates/zipcodes/README.md +0 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/crates/zipcodes/src/zips.json.bz2 +0 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/crates/zipcodes-py/Cargo.toml +0 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/pyproject.toml +0 -0
- {zipcodes-2.0.1 → zipcodes-3.0.0}/python/zipcodes/py.typed +0 -0
|
@@ -216,7 +216,7 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
|
216
216
|
|
|
217
217
|
[[package]]
|
|
218
218
|
name = "zipcodes"
|
|
219
|
-
version = "
|
|
219
|
+
version = "3.0.0"
|
|
220
220
|
dependencies = [
|
|
221
221
|
"bzip2",
|
|
222
222
|
"serde",
|
|
@@ -226,7 +226,7 @@ dependencies = [
|
|
|
226
226
|
|
|
227
227
|
[[package]]
|
|
228
228
|
name = "zipcodes-py"
|
|
229
|
-
version = "
|
|
229
|
+
version = "3.0.0"
|
|
230
230
|
dependencies = [
|
|
231
231
|
"pyo3",
|
|
232
232
|
"serde_json",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zipcodes
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -106,6 +106,17 @@ $ cargo add zipcodes
|
|
|
106
106
|
- Behavioral notes for upgraders: query results are fresh dicts (mutating a
|
|
107
107
|
result no longer mutates the shared database list), and
|
|
108
108
|
`filter_by(active=1)` no longer matches `active=True` (pass a bool).
|
|
109
|
+
- **Unified validation.** Input validation now lives once, in the Rust core,
|
|
110
|
+
instead of a separate Python validator. This changes a few edge cases:
|
|
111
|
+
- **Breaking:** `is_real`/`matching` now raise `ValueError` for input shorter
|
|
112
|
+
than five characters (after trimming), instead of returning `False`/`[]`.
|
|
113
|
+
- **Relaxations:** surrounding whitespace (`" 06903 "`) and space-separated
|
|
114
|
+
Zip+4 (`"12345 6789"`) are now accepted; any input of five or more
|
|
115
|
+
characters is normalized to its first five digits, generalizing the
|
|
116
|
+
existing `#####-####` handling.
|
|
117
|
+
- **Private API removal:** the undocumented helpers `zipcodes._clean` and
|
|
118
|
+
`zipcodes._contains_nondigits` (and the `_digits`/`_valid_zipcode_length`
|
|
119
|
+
module attributes) no longer exist.
|
|
109
120
|
|
|
110
121
|
## Zipcode Data
|
|
111
122
|
|
|
@@ -208,7 +219,7 @@ Traceback (most recent call last):
|
|
|
208
219
|
...
|
|
209
220
|
ValueError: Invalid characters, zipcode may only contain digits and "-".
|
|
210
221
|
|
|
211
|
-
>>> zipcodes.matching('
|
|
222
|
+
>>> zipcodes.matching('0646')
|
|
212
223
|
Traceback (most recent call last):
|
|
213
224
|
...
|
|
214
225
|
ValueError: Invalid format, zipcode must be of the format: "#####" or "#####-####"
|
|
@@ -83,6 +83,17 @@ $ cargo add zipcodes
|
|
|
83
83
|
- Behavioral notes for upgraders: query results are fresh dicts (mutating a
|
|
84
84
|
result no longer mutates the shared database list), and
|
|
85
85
|
`filter_by(active=1)` no longer matches `active=True` (pass a bool).
|
|
86
|
+
- **Unified validation.** Input validation now lives once, in the Rust core,
|
|
87
|
+
instead of a separate Python validator. This changes a few edge cases:
|
|
88
|
+
- **Breaking:** `is_real`/`matching` now raise `ValueError` for input shorter
|
|
89
|
+
than five characters (after trimming), instead of returning `False`/`[]`.
|
|
90
|
+
- **Relaxations:** surrounding whitespace (`" 06903 "`) and space-separated
|
|
91
|
+
Zip+4 (`"12345 6789"`) are now accepted; any input of five or more
|
|
92
|
+
characters is normalized to its first five digits, generalizing the
|
|
93
|
+
existing `#####-####` handling.
|
|
94
|
+
- **Private API removal:** the undocumented helpers `zipcodes._clean` and
|
|
95
|
+
`zipcodes._contains_nondigits` (and the `_digits`/`_valid_zipcode_length`
|
|
96
|
+
module attributes) no longer exist.
|
|
86
97
|
|
|
87
98
|
## Zipcode Data
|
|
88
99
|
|
|
@@ -185,7 +196,7 @@ Traceback (most recent call last):
|
|
|
185
196
|
...
|
|
186
197
|
ValueError: Invalid characters, zipcode may only contain digits and "-".
|
|
187
198
|
|
|
188
|
-
>>> zipcodes.matching('
|
|
199
|
+
>>> zipcodes.matching('0646')
|
|
189
200
|
Traceback (most recent call last):
|
|
190
201
|
...
|
|
191
202
|
ValueError: Invalid format, zipcode must be of the format: "#####" or "#####-####"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//! Emit the canonical record schema as JSON for the data pipeline to consume.
|
|
2
|
+
//!
|
|
3
|
+
//! `scripts/update_zipcode_dataset.py` loads `field_order` from the generated
|
|
4
|
+
//! file instead of hard-coding it, and CI diffs the committed copy against fresh
|
|
5
|
+
//! output so a struct/`FIELD_ORDER` change can't ship without refreshing it.
|
|
6
|
+
//!
|
|
7
|
+
//! Regenerate with:
|
|
8
|
+
//! cargo run -p zipcodes --example gen_schema > scripts/data/schema.json
|
|
9
|
+
fn main() {
|
|
10
|
+
let schema = serde_json::json!({ "field_order": zipcodes::FIELD_ORDER });
|
|
11
|
+
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
|
12
|
+
}
|
|
@@ -56,6 +56,31 @@ pub struct Zipcode {
|
|
|
56
56
|
pub zip_code_type: String,
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/// Canonical field order of records in `zips.json` and of the dicts returned by
|
|
60
|
+
/// the Python package. This is the base CSV's historical column order — **not**
|
|
61
|
+
/// alphabetical (the struct above is declared alphabetically; serde matches by
|
|
62
|
+
/// name, so the two need only agree on the field *set*, not its order).
|
|
63
|
+
///
|
|
64
|
+
/// `lat`/`long` are `String` here and round-trip losslessly through serde; if
|
|
65
|
+
/// they ever become `f64`, the serde-based `to_dict` in the Python binding and
|
|
66
|
+
/// the JSON build pipeline must be revisited together.
|
|
67
|
+
pub const FIELD_ORDER: [&str; 14] = [
|
|
68
|
+
"zip_code",
|
|
69
|
+
"zip_code_type",
|
|
70
|
+
"active",
|
|
71
|
+
"city",
|
|
72
|
+
"acceptable_cities",
|
|
73
|
+
"unacceptable_cities",
|
|
74
|
+
"state",
|
|
75
|
+
"county",
|
|
76
|
+
"timezone",
|
|
77
|
+
"area_codes",
|
|
78
|
+
"world_region",
|
|
79
|
+
"country",
|
|
80
|
+
"lat",
|
|
81
|
+
"long",
|
|
82
|
+
];
|
|
83
|
+
|
|
59
84
|
impl Zipcode {
|
|
60
85
|
/// Compare a named field against a JSON value, mirroring the Python
|
|
61
86
|
/// package's `filter_by(**kwargs)` semantics: an unknown field name or a
|
|
@@ -211,7 +236,10 @@ pub fn database() -> &'static [Zipcode] {
|
|
|
211
236
|
&ZIPCODES
|
|
212
237
|
}
|
|
213
238
|
|
|
214
|
-
|
|
239
|
+
/// Validate and normalize a full zipcode (`"#####"`, `"#####-####"`, or
|
|
240
|
+
/// `"##### ####"`): trim surrounding whitespace, require at least five
|
|
241
|
+
/// characters, then take the first five and require them to be ASCII digits.
|
|
242
|
+
pub fn clean_zipcode(zipcode: &str) -> Result<&str> {
|
|
215
243
|
let zipcode = zipcode.trim();
|
|
216
244
|
if zipcode.len() < ZIPCODE_LENGTH {
|
|
217
245
|
return Err(Error::InvalidFormat);
|
|
@@ -223,11 +251,46 @@ fn clean_zipcode(zipcode: &str) -> Result<&str> {
|
|
|
223
251
|
Ok(prefix)
|
|
224
252
|
}
|
|
225
253
|
|
|
254
|
+
/// Validate a partial zipcode (prefix or fragment): after trimming, require
|
|
255
|
+
/// 1–5 ASCII digits. Longer input is [`Error::InvalidFormat`]; non-digits are
|
|
256
|
+
/// [`Error::InvalidCharacters`]. Empty input is also rejected as
|
|
257
|
+
/// [`Error::InvalidFormat`] (the Python shim guards it earlier with a
|
|
258
|
+
/// `TypeError`), so this never returns `Ok("")`.
|
|
259
|
+
pub fn clean_prefix(partial: &str) -> Result<&str> {
|
|
260
|
+
let partial = partial.trim();
|
|
261
|
+
if partial.is_empty() || partial.len() > ZIPCODE_LENGTH {
|
|
262
|
+
return Err(Error::InvalidFormat);
|
|
263
|
+
}
|
|
264
|
+
if !partial.chars().all(|c| c.is_ascii_digit()) {
|
|
265
|
+
return Err(Error::InvalidCharacters);
|
|
266
|
+
}
|
|
267
|
+
Ok(partial)
|
|
268
|
+
}
|
|
269
|
+
|
|
226
270
|
#[cfg(test)]
|
|
227
271
|
mod tests {
|
|
228
272
|
use super::*;
|
|
229
273
|
use serde_json::json;
|
|
230
274
|
|
|
275
|
+
#[test]
|
|
276
|
+
fn field_order_matches_struct() {
|
|
277
|
+
// FIELD_ORDER must list exactly the struct's serialized fields. A field
|
|
278
|
+
// added to one but not the other fails here rather than silently
|
|
279
|
+
// dropping from the dicts the binding builds.
|
|
280
|
+
let record = &matching("06903", None).unwrap()[0];
|
|
281
|
+
let value = serde_json::to_value(record).unwrap();
|
|
282
|
+
let mut keys: Vec<&str> = value
|
|
283
|
+
.as_object()
|
|
284
|
+
.unwrap()
|
|
285
|
+
.keys()
|
|
286
|
+
.map(String::as_str)
|
|
287
|
+
.collect();
|
|
288
|
+
keys.sort_unstable();
|
|
289
|
+
let mut expected = FIELD_ORDER.to_vec();
|
|
290
|
+
expected.sort_unstable();
|
|
291
|
+
assert_eq!(keys, expected);
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
#[test]
|
|
232
295
|
fn should_find_real_zipcodes() {
|
|
233
296
|
assert!(is_real("06903").unwrap());
|
|
@@ -295,6 +358,40 @@ mod tests {
|
|
|
295
358
|
));
|
|
296
359
|
}
|
|
297
360
|
|
|
361
|
+
#[test]
|
|
362
|
+
fn clean_zipcode_normalizes_valid_input() {
|
|
363
|
+
assert_eq!(clean_zipcode("12345").unwrap(), "12345");
|
|
364
|
+
assert_eq!(clean_zipcode("12345-6789").unwrap(), "12345");
|
|
365
|
+
assert_eq!(clean_zipcode("12345 6789").unwrap(), "12345");
|
|
366
|
+
assert_eq!(clean_zipcode(" 06903 ").unwrap(), "06903");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn clean_zipcode_rejects_invalid_input() {
|
|
371
|
+
assert!(matches!(clean_zipcode("123"), Err(Error::InvalidFormat)));
|
|
372
|
+
assert!(matches!(clean_zipcode(" "), Err(Error::InvalidFormat)));
|
|
373
|
+
assert!(matches!(
|
|
374
|
+
clean_zipcode("1234a"),
|
|
375
|
+
Err(Error::InvalidCharacters)
|
|
376
|
+
));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn clean_prefix_accepts_one_to_five_digits() {
|
|
381
|
+
for p in ["1", "12", "123", "1234", "12345"] {
|
|
382
|
+
assert_eq!(clean_prefix(p).unwrap(), p);
|
|
383
|
+
}
|
|
384
|
+
assert_eq!(clean_prefix(" 10 ").unwrap(), "10");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[test]
|
|
388
|
+
fn clean_prefix_rejects_invalid_input() {
|
|
389
|
+
assert!(matches!(clean_prefix("123456"), Err(Error::InvalidFormat)));
|
|
390
|
+
assert!(matches!(clean_prefix(""), Err(Error::InvalidFormat)));
|
|
391
|
+
assert!(matches!(clean_prefix(" "), Err(Error::InvalidFormat)));
|
|
392
|
+
assert!(matches!(clean_prefix("10a"), Err(Error::InvalidCharacters)));
|
|
393
|
+
}
|
|
394
|
+
|
|
298
395
|
#[test]
|
|
299
396
|
fn should_include_county_field() {
|
|
300
397
|
let zips = matching("06475", None).unwrap();
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//! Native module `zipcodes._zipcodes`.
|
|
2
|
+
//!
|
|
3
|
+
//! Database scans delegate to the `zipcodes` crate's query functions, and the
|
|
4
|
+
//! `zips=` override path (chaining filters over previously returned dicts) is
|
|
5
|
+
//! filtered here in Rust, so the crate is the single implementation of scan
|
|
6
|
+
//! semantics. The Python-facing compat layer (argument validation, exact 1.x
|
|
7
|
+
//! exception messages) lives in `python/zipcodes/__init__.py`.
|
|
8
|
+
|
|
9
|
+
use pyo3::exceptions::PyValueError;
|
|
10
|
+
use pyo3::prelude::*;
|
|
11
|
+
use pyo3::types::{PyBool, PyDict, PyList};
|
|
12
|
+
use serde_json::Value;
|
|
13
|
+
use zipcodes::Zipcode;
|
|
14
|
+
|
|
15
|
+
/// Map a core validation error to the 1.x `ValueError`, preserving its message
|
|
16
|
+
/// verbatim. A free helper rather than `impl From<zipcodes::Error> for PyErr`
|
|
17
|
+
/// because both types are foreign to this crate (orphan rule).
|
|
18
|
+
fn to_py_err(e: zipcodes::Error) -> PyErr {
|
|
19
|
+
PyValueError::new_err(e.to_string())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Build a dict in `zipcodes::FIELD_ORDER` order (the 1.x pure-Python key order).
|
|
23
|
+
///
|
|
24
|
+
/// This is hand-written rather than derived from `serde_json::to_value` on
|
|
25
|
+
/// purpose: a serde-based builder iterating `FIELD_ORDER` cannot drift from the
|
|
26
|
+
/// struct, but it allocates a `Map<String, Value>` per record and measured ~60%
|
|
27
|
+
/// slower on `list_all()` (which materializes ~42k dicts) — past our tolerance.
|
|
28
|
+
/// Drift is instead caught by tests, not by construction: `FIELD_ORDER` is
|
|
29
|
+
/// pinned to the struct by `field_order_matches_struct` in the core crate, and
|
|
30
|
+
/// this builder's output is pinned to `FIELD_ORDER` by `tests/test_schema.py`.
|
|
31
|
+
/// Keep the `set_item` order identical to `FIELD_ORDER`.
|
|
32
|
+
fn to_dict<'py>(py: Python<'py>, z: &Zipcode) -> PyResult<Bound<'py, PyDict>> {
|
|
33
|
+
let dict = PyDict::new(py);
|
|
34
|
+
dict.set_item("zip_code", &z.zip_code)?;
|
|
35
|
+
dict.set_item("zip_code_type", &z.zip_code_type)?;
|
|
36
|
+
dict.set_item("active", z.active)?;
|
|
37
|
+
dict.set_item("city", &z.city)?;
|
|
38
|
+
dict.set_item("acceptable_cities", &z.acceptable_cities)?;
|
|
39
|
+
dict.set_item("unacceptable_cities", &z.unacceptable_cities)?;
|
|
40
|
+
dict.set_item("state", &z.state)?;
|
|
41
|
+
dict.set_item("county", &z.county)?;
|
|
42
|
+
dict.set_item("timezone", &z.timezone)?;
|
|
43
|
+
dict.set_item("area_codes", &z.area_codes)?;
|
|
44
|
+
dict.set_item("world_region", &z.world_region)?;
|
|
45
|
+
dict.set_item("country", &z.country)?;
|
|
46
|
+
dict.set_item("lat", &z.lat)?;
|
|
47
|
+
dict.set_item("long", &z.long)?;
|
|
48
|
+
Ok(dict)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Convert crate query results to a list of dicts (1.x field order).
|
|
52
|
+
fn to_pylist<'py>(py: Python<'py>, zips: &[Zipcode]) -> PyResult<Bound<'py, PyList>> {
|
|
53
|
+
let dicts = zips
|
|
54
|
+
.iter()
|
|
55
|
+
.map(|z| to_dict(py, z))
|
|
56
|
+
.collect::<PyResult<Vec<_>>>()?;
|
|
57
|
+
PyList::new(py, dicts)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Filter a caller-supplied list of dicts, returning the matching items as-is
|
|
61
|
+
/// (no deserialization round-trip, no re-ordering of caller keys). The
|
|
62
|
+
/// predicate treats any per-item failure (missing key, non-dict item,
|
|
63
|
+
/// inconvertible value) as "not a match".
|
|
64
|
+
fn filter_pylist<'py, F>(
|
|
65
|
+
py: Python<'py>,
|
|
66
|
+
zips: &Bound<'py, PyList>,
|
|
67
|
+
pred: F,
|
|
68
|
+
) -> PyResult<Bound<'py, PyList>>
|
|
69
|
+
where
|
|
70
|
+
F: Fn(&Bound<'py, PyAny>) -> bool,
|
|
71
|
+
{
|
|
72
|
+
let matches: Vec<_> = zips.iter().filter(|item| pred(item)).collect();
|
|
73
|
+
PyList::new(py, matches)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Extract `item[key]` as a string; `None` on a missing key or non-string value.
|
|
77
|
+
fn get_str(item: &Bound<'_, PyAny>, key: &str) -> Option<String> {
|
|
78
|
+
item.get_item(key).ok()?.extract::<String>().ok()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Extract `item[key]` as a coordinate, accepting str or float values as the
|
|
82
|
+
/// 1.x `float(z["lat"])` did.
|
|
83
|
+
fn get_coordinate(item: &Bound<'_, PyAny>, key: &str) -> Option<f64> {
|
|
84
|
+
let value = item.get_item(key).ok()?;
|
|
85
|
+
if let Ok(f) = value.extract::<f64>() {
|
|
86
|
+
return Some(f);
|
|
87
|
+
}
|
|
88
|
+
value.extract::<String>().ok()?.parse::<f64>().ok()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Convert a Python filter value to JSON for comparison against record fields.
|
|
92
|
+
/// Returns None for values no record field could ever equal (e.g. sets,
|
|
93
|
+
/// arbitrary objects), which the caller treats as "matches nothing".
|
|
94
|
+
fn py_to_json(value: &Bound<'_, PyAny>) -> Option<Value> {
|
|
95
|
+
if value.is_none() {
|
|
96
|
+
return Some(Value::Null);
|
|
97
|
+
}
|
|
98
|
+
if let Ok(b) = value.cast::<PyBool>() {
|
|
99
|
+
return Some(Value::Bool(b.is_true()));
|
|
100
|
+
}
|
|
101
|
+
if let Ok(i) = value.extract::<i64>() {
|
|
102
|
+
return Some(Value::from(i));
|
|
103
|
+
}
|
|
104
|
+
if let Ok(f) = value.extract::<f64>() {
|
|
105
|
+
return Some(Value::from(f));
|
|
106
|
+
}
|
|
107
|
+
if let Ok(s) = value.extract::<String>() {
|
|
108
|
+
return Some(Value::String(s));
|
|
109
|
+
}
|
|
110
|
+
if let Ok(list) = value.cast::<PyList>() {
|
|
111
|
+
let mut arr = Vec::with_capacity(list.len());
|
|
112
|
+
for item in list.iter() {
|
|
113
|
+
arr.push(py_to_json(&item)?);
|
|
114
|
+
}
|
|
115
|
+
return Some(Value::Array(arr));
|
|
116
|
+
}
|
|
117
|
+
None
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[pyfunction]
|
|
121
|
+
#[pyo3(signature = (zipcode, zips=None))]
|
|
122
|
+
fn matching<'py>(
|
|
123
|
+
py: Python<'py>,
|
|
124
|
+
zipcode: &str,
|
|
125
|
+
zips: Option<Bound<'py, PyList>>,
|
|
126
|
+
) -> PyResult<Bound<'py, PyList>> {
|
|
127
|
+
match zips {
|
|
128
|
+
None => {
|
|
129
|
+
let found = zipcodes::matching(zipcode, None).map_err(to_py_err)?;
|
|
130
|
+
to_pylist(py, &found)
|
|
131
|
+
}
|
|
132
|
+
Some(zips) => {
|
|
133
|
+
let zipcode = zipcodes::clean_zipcode(zipcode).map_err(to_py_err)?;
|
|
134
|
+
filter_pylist(py, &zips, |item| {
|
|
135
|
+
get_str(item, "zip_code").as_deref() == Some(zipcode)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[pyfunction]
|
|
142
|
+
fn is_real(zipcode: &str) -> PyResult<bool> {
|
|
143
|
+
zipcodes::is_real(zipcode).map_err(to_py_err)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[pyfunction]
|
|
147
|
+
#[pyo3(signature = (prefix, zips=None))]
|
|
148
|
+
fn similar_to<'py>(
|
|
149
|
+
py: Python<'py>,
|
|
150
|
+
prefix: &str,
|
|
151
|
+
zips: Option<Bound<'py, PyList>>,
|
|
152
|
+
) -> PyResult<Bound<'py, PyList>> {
|
|
153
|
+
let prefix = zipcodes::clean_prefix(prefix).map_err(to_py_err)?;
|
|
154
|
+
match zips {
|
|
155
|
+
None => to_pylist(py, &zipcodes::similar_to(prefix, None)),
|
|
156
|
+
Some(zips) => filter_pylist(py, &zips, |item| {
|
|
157
|
+
get_str(item, "zip_code").is_some_and(|zc| zc.starts_with(prefix))
|
|
158
|
+
}),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[pyfunction]
|
|
163
|
+
#[pyo3(signature = (fragment, zips=None))]
|
|
164
|
+
fn contains<'py>(
|
|
165
|
+
py: Python<'py>,
|
|
166
|
+
fragment: &str,
|
|
167
|
+
zips: Option<Bound<'py, PyList>>,
|
|
168
|
+
) -> PyResult<Bound<'py, PyList>> {
|
|
169
|
+
let fragment = zipcodes::clean_prefix(fragment).map_err(to_py_err)?;
|
|
170
|
+
match zips {
|
|
171
|
+
None => to_pylist(py, &zipcodes::contains(fragment, None)),
|
|
172
|
+
Some(zips) => filter_pylist(py, &zips, |item| {
|
|
173
|
+
get_str(item, "zip_code").is_some_and(|zc| zc.contains(fragment))
|
|
174
|
+
}),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[pyfunction]
|
|
179
|
+
#[pyo3(signature = (zips=None, **kwargs))]
|
|
180
|
+
fn filter_by<'py>(
|
|
181
|
+
py: Python<'py>,
|
|
182
|
+
zips: Option<Bound<'py, PyList>>,
|
|
183
|
+
kwargs: Option<&Bound<'py, PyDict>>,
|
|
184
|
+
) -> PyResult<Bound<'py, PyList>> {
|
|
185
|
+
let mut filters = Vec::new();
|
|
186
|
+
if let Some(kwargs) = kwargs {
|
|
187
|
+
for (key, value) in kwargs.iter() {
|
|
188
|
+
let key: String = key.extract()?;
|
|
189
|
+
match py_to_json(&value) {
|
|
190
|
+
Some(value) => filters.push((key, value)),
|
|
191
|
+
// A value of an inconvertible type can never equal a record
|
|
192
|
+
// field, and all filters must match.
|
|
193
|
+
None => return Ok(PyList::empty(py)),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
match zips {
|
|
198
|
+
None => to_pylist(py, &zipcodes::filter_by_fields(&filters, None)),
|
|
199
|
+
// Both sides of the comparison go through `py_to_json`, never Python
|
|
200
|
+
// rich comparison, so e.g. `active=1` does not match a True field.
|
|
201
|
+
Some(zips) => filter_pylist(py, &zips, |item| {
|
|
202
|
+
filters.iter().all(|(key, value)| {
|
|
203
|
+
item.get_item(key.as_str())
|
|
204
|
+
.ok()
|
|
205
|
+
.and_then(|v| py_to_json(&v))
|
|
206
|
+
.as_ref()
|
|
207
|
+
== Some(value)
|
|
208
|
+
})
|
|
209
|
+
}),
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[pyfunction]
|
|
214
|
+
#[pyo3(signature = (lat, long, radius_in_miles, zips=None))]
|
|
215
|
+
fn filter_by_coordinates<'py>(
|
|
216
|
+
py: Python<'py>,
|
|
217
|
+
lat: f64,
|
|
218
|
+
long: f64,
|
|
219
|
+
radius_in_miles: f64,
|
|
220
|
+
zips: Option<Bound<'py, PyList>>,
|
|
221
|
+
) -> PyResult<Bound<'py, PyList>> {
|
|
222
|
+
match zips {
|
|
223
|
+
None => to_pylist(
|
|
224
|
+
py,
|
|
225
|
+
&zipcodes::filter_by_coordinates(lat, long, radius_in_miles, None),
|
|
226
|
+
),
|
|
227
|
+
Some(zips) => filter_pylist(py, &zips, |item| {
|
|
228
|
+
match (get_coordinate(item, "lat"), get_coordinate(item, "long")) {
|
|
229
|
+
(Some(z_lat), Some(z_long)) => {
|
|
230
|
+
zipcodes::haversine(z_long, z_lat, long, lat) <= radius_in_miles
|
|
231
|
+
}
|
|
232
|
+
_ => false,
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[pyfunction]
|
|
239
|
+
fn haversine(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
|
|
240
|
+
zipcodes::haversine(lon1, lat1, lon2, lat2)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[pyfunction]
|
|
244
|
+
fn list_all<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
|
|
245
|
+
to_pylist(py, zipcodes::database())
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[pymodule]
|
|
249
|
+
fn _zipcodes(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
250
|
+
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
|
251
|
+
m.add("FIELD_ORDER", zipcodes::FIELD_ORDER.to_vec())?;
|
|
252
|
+
m.add_function(wrap_pyfunction!(matching, m)?)?;
|
|
253
|
+
m.add_function(wrap_pyfunction!(is_real, m)?)?;
|
|
254
|
+
m.add_function(wrap_pyfunction!(similar_to, m)?)?;
|
|
255
|
+
m.add_function(wrap_pyfunction!(contains, m)?)?;
|
|
256
|
+
m.add_function(wrap_pyfunction!(filter_by, m)?)?;
|
|
257
|
+
m.add_function(wrap_pyfunction!(filter_by_coordinates, m)?)?;
|
|
258
|
+
m.add_function(wrap_pyfunction!(haversine, m)?)?;
|
|
259
|
+
m.add_function(wrap_pyfunction!(list_all, m)?)?;
|
|
260
|
+
Ok(())
|
|
261
|
+
}
|
|
@@ -7,13 +7,11 @@ No-SQLite U.S. zipcode validation Python package, ready for use in AWS Lambda
|
|
|
7
7
|
:author: Sean Pianka
|
|
8
8
|
:github: @seanpianka
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
(``zipcodes._zipcodes``); this module preserves
|
|
12
|
-
|
|
10
|
+
All queries — full-database scans and ``zips=`` chaining lists alike — run in
|
|
11
|
+
the compiled Rust extension (``zipcodes._zipcodes``); this module preserves
|
|
12
|
+
the exact 1.x behavior for argument validation and exceptions.
|
|
13
13
|
"""
|
|
14
|
-
import re
|
|
15
14
|
import warnings
|
|
16
|
-
from math import asin, cos, radians, sin, sqrt
|
|
17
15
|
|
|
18
16
|
from zipcodes import _zipcodes
|
|
19
17
|
|
|
@@ -22,9 +20,6 @@ __email__ = "pianka@eml.cc"
|
|
|
22
20
|
__license__ = "MIT"
|
|
23
21
|
__version__ = _zipcodes.__version__
|
|
24
22
|
|
|
25
|
-
_digits = re.compile(r"[^\d\-]")
|
|
26
|
-
_valid_zipcode_length = 5
|
|
27
|
-
|
|
28
23
|
_zips_cache = None
|
|
29
24
|
|
|
30
25
|
|
|
@@ -43,52 +38,42 @@ def __getattr__(name):
|
|
|
43
38
|
raise AttributeError("module {!r} has no attribute {!r}".format(__name__, name))
|
|
44
39
|
|
|
45
40
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
_clean(zipcode, min(len(zipcode), _valid_zipcode_length)), *args, **kwargs
|
|
53
|
-
)
|
|
41
|
+
def _require_str(zipcode):
|
|
42
|
+
# The only validation that must stay in Python: PyO3's auto-generated
|
|
43
|
+
# TypeError for a non-str argument has a different message than 1.x. All
|
|
44
|
+
# format/character validation lives in the Rust core.
|
|
45
|
+
if not zipcode or not isinstance(zipcode, str):
|
|
46
|
+
raise TypeError("Invalid type, zipcode must be a string.")
|
|
54
47
|
|
|
55
|
-
return decorator
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
@_clean_zipcode
|
|
59
49
|
def matching(zipcode, zips=None):
|
|
60
50
|
"""Retrieve zipcode dict for provided zipcode"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return [z for z in zips if z["zip_code"] == zipcode]
|
|
51
|
+
_require_str(zipcode)
|
|
52
|
+
return _zipcodes.matching(zipcode, zips=zips)
|
|
64
53
|
|
|
65
54
|
|
|
66
|
-
@_clean_zipcode
|
|
67
55
|
def is_valid(zipcode):
|
|
56
|
+
_require_str(zipcode)
|
|
68
57
|
warnings.warn("is_valid is deprecated; use is_real", DeprecationWarning, stacklevel=2)
|
|
69
58
|
return is_real(zipcode)
|
|
70
59
|
|
|
71
60
|
|
|
72
|
-
@_clean_zipcode
|
|
73
61
|
def is_real(zipcode):
|
|
74
62
|
"""Determine whether a given zip or zip+4 zipcode is real."""
|
|
63
|
+
_require_str(zipcode)
|
|
75
64
|
return _zipcodes.is_real(zipcode)
|
|
76
65
|
|
|
77
66
|
|
|
78
|
-
@_clean_zipcode
|
|
79
67
|
def similar_to(partial_zipcode, zips=None):
|
|
80
68
|
"""List of zipcode dicts where zipcode prefix matches `partial_zipcode`"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return [z for z in zips if z["zip_code"].startswith(partial_zipcode)]
|
|
69
|
+
_require_str(partial_zipcode)
|
|
70
|
+
return _zipcodes.similar_to(partial_zipcode, zips=zips)
|
|
84
71
|
|
|
85
72
|
|
|
86
|
-
@_clean_zipcode
|
|
87
73
|
def contains(partial_zipcode, zips=None):
|
|
88
74
|
"""List of zipcode dicts where zipcode contains `partial_zipcode` fragment"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return [z for z in zips if partial_zipcode in z["zip_code"]]
|
|
75
|
+
_require_str(partial_zipcode)
|
|
76
|
+
return _zipcodes.contains(partial_zipcode, zips=zips)
|
|
92
77
|
|
|
93
78
|
|
|
94
79
|
def filter_by_state(state, zips=None):
|
|
@@ -116,36 +101,17 @@ def haversine(lon1, lat1, lon2, lat2):
|
|
|
116
101
|
Calculate the great circle distance in miles between two points
|
|
117
102
|
on the earth (specified in decimal degrees)
|
|
118
103
|
"""
|
|
119
|
-
|
|
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
|
|
104
|
+
return _zipcodes.haversine(lon1, lat1, lon2, lat2)
|
|
127
105
|
|
|
128
106
|
|
|
129
107
|
def filter_by_coordinates(lat, long, radius_in_miles=10, zips=None):
|
|
130
108
|
"""List of zipcode dicts within `radius_in_miles` of (`lat`, `long`)."""
|
|
131
|
-
|
|
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
|
-
]
|
|
109
|
+
return _zipcodes.filter_by_coordinates(lat, long, radius_in_miles, zips=zips)
|
|
138
110
|
|
|
139
111
|
|
|
140
112
|
def filter_by(zips=None, **filters):
|
|
141
113
|
"""Use `kwargs` to select for desired attributes from list of zipcode dicts"""
|
|
142
|
-
|
|
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
|
-
]
|
|
114
|
+
return _zipcodes.filter_by(zips, **filters)
|
|
149
115
|
|
|
150
116
|
|
|
151
117
|
def list_all(zips=None):
|
|
@@ -153,22 +119,3 @@ def list_all(zips=None):
|
|
|
153
119
|
if zips is None:
|
|
154
120
|
return _load_zips()
|
|
155
121
|
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
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
__version__: str
|
|
4
|
+
FIELD_ORDER: List[str]
|
|
5
|
+
|
|
6
|
+
def matching(
|
|
7
|
+
zipcode: str, zips: Optional[List[Dict[str, Any]]] = None
|
|
8
|
+
) -> List[Dict[str, Any]]: ...
|
|
9
|
+
def is_real(zipcode: str) -> bool: ...
|
|
10
|
+
def similar_to(
|
|
11
|
+
prefix: str, zips: Optional[List[Dict[str, Any]]] = None
|
|
12
|
+
) -> List[Dict[str, Any]]: ...
|
|
13
|
+
def contains(
|
|
14
|
+
fragment: str, zips: Optional[List[Dict[str, Any]]] = None
|
|
15
|
+
) -> List[Dict[str, Any]]: ...
|
|
16
|
+
def filter_by(
|
|
17
|
+
zips: Optional[List[Dict[str, Any]]] = None, **filters: Any
|
|
18
|
+
) -> List[Dict[str, Any]]: ...
|
|
19
|
+
def filter_by_coordinates(
|
|
20
|
+
lat: float,
|
|
21
|
+
long: float,
|
|
22
|
+
radius_in_miles: float,
|
|
23
|
+
zips: Optional[List[Dict[str, Any]]] = None,
|
|
24
|
+
) -> List[Dict[str, Any]]: ...
|
|
25
|
+
def haversine(lon1: float, lat1: float, lon2: float, lat2: float) -> float: ...
|
|
26
|
+
def list_all() -> List[Dict[str, Any]]: ...
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
//! Native module `zipcodes._zipcodes`.
|
|
2
|
-
//!
|
|
3
|
-
//! The Python-facing compat layer (validation, exact 1.x exception messages,
|
|
4
|
-
//! `zips=` chaining over caller-supplied dicts) lives in
|
|
5
|
-
//! `python/zipcodes/__init__.py`; this module only handles scans over the
|
|
6
|
-
//! embedded database, materializing matching records as Python dicts.
|
|
7
|
-
|
|
8
|
-
use pyo3::prelude::*;
|
|
9
|
-
use pyo3::types::{PyBool, PyDict, PyList};
|
|
10
|
-
use serde_json::Value;
|
|
11
|
-
use zipcodes::Zipcode;
|
|
12
|
-
|
|
13
|
-
/// Build a dict with the same field order the 1.x pure-Python package produced.
|
|
14
|
-
fn to_dict<'py>(py: Python<'py>, z: &Zipcode) -> PyResult<Bound<'py, PyDict>> {
|
|
15
|
-
let dict = PyDict::new(py);
|
|
16
|
-
dict.set_item("zip_code", &z.zip_code)?;
|
|
17
|
-
dict.set_item("zip_code_type", &z.zip_code_type)?;
|
|
18
|
-
dict.set_item("active", z.active)?;
|
|
19
|
-
dict.set_item("city", &z.city)?;
|
|
20
|
-
dict.set_item("acceptable_cities", &z.acceptable_cities)?;
|
|
21
|
-
dict.set_item("unacceptable_cities", &z.unacceptable_cities)?;
|
|
22
|
-
dict.set_item("state", &z.state)?;
|
|
23
|
-
dict.set_item("county", &z.county)?;
|
|
24
|
-
dict.set_item("timezone", &z.timezone)?;
|
|
25
|
-
dict.set_item("area_codes", &z.area_codes)?;
|
|
26
|
-
dict.set_item("world_region", &z.world_region)?;
|
|
27
|
-
dict.set_item("country", &z.country)?;
|
|
28
|
-
dict.set_item("lat", &z.lat)?;
|
|
29
|
-
dict.set_item("long", &z.long)?;
|
|
30
|
-
Ok(dict)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
fn collect<'py, F>(py: Python<'py>, predicate: F) -> PyResult<Bound<'py, PyList>>
|
|
34
|
-
where
|
|
35
|
-
F: Fn(&Zipcode) -> bool,
|
|
36
|
-
{
|
|
37
|
-
let dicts = zipcodes::database()
|
|
38
|
-
.iter()
|
|
39
|
-
.filter(|z| predicate(z))
|
|
40
|
-
.map(|z| to_dict(py, z))
|
|
41
|
-
.collect::<PyResult<Vec<_>>>()?;
|
|
42
|
-
PyList::new(py, dicts)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Convert a Python filter value to JSON for comparison against record fields.
|
|
46
|
-
/// Returns None for values no record field could ever equal (e.g. sets,
|
|
47
|
-
/// arbitrary objects), which the caller treats as "matches nothing".
|
|
48
|
-
fn py_to_json(value: &Bound<'_, PyAny>) -> Option<Value> {
|
|
49
|
-
if value.is_none() {
|
|
50
|
-
return Some(Value::Null);
|
|
51
|
-
}
|
|
52
|
-
if let Ok(b) = value.cast::<PyBool>() {
|
|
53
|
-
return Some(Value::Bool(b.is_true()));
|
|
54
|
-
}
|
|
55
|
-
if let Ok(i) = value.extract::<i64>() {
|
|
56
|
-
return Some(Value::from(i));
|
|
57
|
-
}
|
|
58
|
-
if let Ok(f) = value.extract::<f64>() {
|
|
59
|
-
return Some(Value::from(f));
|
|
60
|
-
}
|
|
61
|
-
if let Ok(s) = value.extract::<String>() {
|
|
62
|
-
return Some(Value::String(s));
|
|
63
|
-
}
|
|
64
|
-
if let Ok(list) = value.cast::<PyList>() {
|
|
65
|
-
let mut arr = Vec::with_capacity(list.len());
|
|
66
|
-
for item in list.iter() {
|
|
67
|
-
arr.push(py_to_json(&item)?);
|
|
68
|
-
}
|
|
69
|
-
return Some(Value::Array(arr));
|
|
70
|
-
}
|
|
71
|
-
None
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/// `zipcode` arrives pre-validated by the Python shim (digits only, length <= 5).
|
|
75
|
-
#[pyfunction]
|
|
76
|
-
fn matching<'py>(py: Python<'py>, zipcode: &str) -> PyResult<Bound<'py, PyList>> {
|
|
77
|
-
collect(py, |z| z.zip_code == zipcode)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
#[pyfunction]
|
|
81
|
-
fn is_real(zipcode: &str) -> bool {
|
|
82
|
-
zipcodes::database().iter().any(|z| z.zip_code == zipcode)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
#[pyfunction]
|
|
86
|
-
fn similar_to<'py>(py: Python<'py>, prefix: &str) -> PyResult<Bound<'py, PyList>> {
|
|
87
|
-
collect(py, |z| z.zip_code.starts_with(prefix))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#[pyfunction]
|
|
91
|
-
fn contains<'py>(py: Python<'py>, fragment: &str) -> PyResult<Bound<'py, PyList>> {
|
|
92
|
-
collect(py, |z| z.zip_code.contains(fragment))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
#[pyfunction]
|
|
96
|
-
#[pyo3(signature = (**kwargs))]
|
|
97
|
-
fn filter_by<'py>(
|
|
98
|
-
py: Python<'py>,
|
|
99
|
-
kwargs: Option<&Bound<'py, PyDict>>,
|
|
100
|
-
) -> PyResult<Bound<'py, PyList>> {
|
|
101
|
-
let mut filters = Vec::new();
|
|
102
|
-
if let Some(kwargs) = kwargs {
|
|
103
|
-
for (key, value) in kwargs.iter() {
|
|
104
|
-
let key: String = key.extract()?;
|
|
105
|
-
match py_to_json(&value) {
|
|
106
|
-
Some(value) => filters.push((key, value)),
|
|
107
|
-
// A value of an inconvertible type can never equal a record
|
|
108
|
-
// field, and all filters must match.
|
|
109
|
-
None => return Ok(PyList::empty(py)),
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
let filters = filters; // freeze for the closure
|
|
114
|
-
collect(py, |z| filters.iter().all(|(k, v)| z.field_matches(k, v)))
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#[pyfunction]
|
|
118
|
-
fn filter_by_coordinates<'py>(
|
|
119
|
-
py: Python<'py>,
|
|
120
|
-
lat: f64,
|
|
121
|
-
long: f64,
|
|
122
|
-
radius_in_miles: f64,
|
|
123
|
-
) -> PyResult<Bound<'py, PyList>> {
|
|
124
|
-
collect(py, |z| {
|
|
125
|
-
match (z.lat.parse::<f64>(), z.long.parse::<f64>()) {
|
|
126
|
-
(Ok(z_lat), Ok(z_long)) => {
|
|
127
|
-
zipcodes::haversine(z_long, z_lat, long, lat) <= radius_in_miles
|
|
128
|
-
}
|
|
129
|
-
_ => false,
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
#[pyfunction]
|
|
135
|
-
fn haversine(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
|
|
136
|
-
zipcodes::haversine(lon1, lat1, lon2, lat2)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
#[pyfunction]
|
|
140
|
-
fn list_all<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
|
|
141
|
-
collect(py, |_| true)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
#[pymodule]
|
|
145
|
-
fn _zipcodes(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
146
|
-
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
|
147
|
-
m.add_function(wrap_pyfunction!(matching, m)?)?;
|
|
148
|
-
m.add_function(wrap_pyfunction!(is_real, m)?)?;
|
|
149
|
-
m.add_function(wrap_pyfunction!(similar_to, m)?)?;
|
|
150
|
-
m.add_function(wrap_pyfunction!(contains, m)?)?;
|
|
151
|
-
m.add_function(wrap_pyfunction!(filter_by, m)?)?;
|
|
152
|
-
m.add_function(wrap_pyfunction!(filter_by_coordinates, m)?)?;
|
|
153
|
-
m.add_function(wrap_pyfunction!(haversine, m)?)?;
|
|
154
|
-
m.add_function(wrap_pyfunction!(list_all, m)?)?;
|
|
155
|
-
Ok(())
|
|
156
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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]]: ...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|