Skip to content

Commit 8e7b9ed

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/analytics-dashboard
2 parents a62f63f + 1c07228 commit 8e7b9ed

5 files changed

Lines changed: 159 additions & 70 deletions

File tree

README.md

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,122 @@
1-
# Postal Regex
21

3-
A community-maintained repository of postal/ZIP code regex patterns for 50+ countries.
4-
This package is ideal for **form validation, data cleaning, and big data applications**.
2+
# Postal Regex 📨
3+
4+
[![PyPI version](https://img.shields.io/pypi/v/postal-regex.svg)](https://pypi.org/project/postal-regex/)
5+
[![License](https://img.shields.io/pypi/l/postal-regex)](LICENSE)
6+
[![Build Status](https://github.com/ankitgadling/postal-regex/actions/workflows/ci.yml/badge.svg)](https://github.com/ankitgadling/postal-regex/actions)
7+
8+
---
9+
10+
A community-maintained repository of postal/ZIP code regex patterns for 50+ countries.
11+
Ideal for **form validation, data cleaning, and big data applications**.
12+
13+
---
14+
15+
## Table of Contents
16+
17+
- [Features](#features)
18+
- [Installation](#installation)
19+
- [Usage](#usage)
20+
- [Big Data Support](#big-data-support)
21+
- [Contributing](#contributing)
22+
- [License](#license)
523

624
---
725

826
## Features
927

1028
- ✅ 50+ countries included, with postal code regex patterns
11-
- ✅ Supports lookup by **country code** or **country name**
12-
- ✅ Precompiled regex for fast validation in Python
13-
- ✅ JSON schema ensures consistent data structure
14-
- ✅ Ready for **big data frameworks** like Spark, Dask, or Pandas
29+
- ✅ Validate postal codes by **country code** or **country name**
30+
```python
31+
from postal_regex.core import validate
32+
33+
validate("IN", "110001") # True
34+
validate("India", "110001") # True
35+
validate("US", "12345-6789") # True
36+
````
37+
38+
* ✅ Normalize country identifiers
39+
40+
```python
41+
from postal_regex.core import normalize
42+
43+
normalize("United States") # "US"
44+
normalize("India") # "IN"
45+
```
46+
47+
* ✅ Works with **Pandas and Spark DataFrames**
48+
49+
```python
50+
import pandas as pd
51+
from postal_regex.bulk import validate_dataframe
52+
53+
df = pd.DataFrame({"country": ["US", "FR"], "postal_code": ["90210", "75001"]})
54+
df_validated = validate_dataframe(df, country_col="country", postal_col="postal_code")
55+
print(df_validated)
56+
```
57+
58+
* ✅ JSON schema ensures consistent data structure
59+
* ✅ Precompiled regex for fast Python validation
1560

1661
---
62+
1763
## Installation
1864

1965
```bash
2066
pip install postal-regex
2167
```
2268

23-
## Usage
69+
For development:
2470

2571
```bash
26-
from postal_regex.core import validate, normalize, get_supported_countries
72+
git clone https://github.com/ankitgadling/postal-regex.git
73+
cd postal-regex
74+
pip install -e .
75+
```
2776

28-
# Validate postal codes
29-
validate("IN", "110001") # True
30-
validate("India", "110001") # True
31-
validate("US", "12345-6789") # True
77+
---
78+
79+
## Big Data Support
3280

33-
# Normalize country identifiers
34-
normalize("India") # "IN"
35-
normalize("US") # "US"
81+
Validate postal codes in **large datasets** with Spark or Pandas.
3682

37-
# List all supported countries
38-
get_supported_countries()
39-
# [{'code': 'IN', 'name': 'India'}, {'code': 'US', 'name': 'United States'}, ...]
83+
### Spark Example
4084

85+
```python
86+
from pyspark.sql import SparkSession
87+
from postal_regex.bulk import validate_spark_dataframe
88+
89+
spark = SparkSession.builder.getOrCreate()
90+
df = spark.createDataFrame([
91+
{"country": "FR", "postal_code": "75001"},
92+
{"country": "DE", "postal_code": "10115"}
93+
])
94+
df_validated = validate_spark_dataframe(df, country_col="country", postal_col="postal_code")
95+
df_validated.show()
4196
```
97+
98+
### Pandas Example
99+
100+
```python
101+
import pandas as pd
102+
from postal_regex.bulk import validate_dataframe
103+
104+
df = pd.DataFrame({
105+
"country": ["FR", "DE"],
106+
"postal_code": ["75001", "10115"]
107+
})
108+
df_validated = validate_dataframe(df, country_col="country", postal_col="postal_code")
109+
print(df_validated)
110+
```
111+
42112
---
43113

44-
## Contributors
114+
## Contributing
115+
116+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
117+
118+
---
45119

46-
[![Contributors](https://contrib.rocks/image?repo=ankitgadling/postal-regex)](https://github.com/ankitgadling/postal-regex/graphs/contributors)
120+
## License
47121

48-
---
122+
MIT License. See [LICENSE](LICENSE) for details.

src/postal_regex/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@
4848
"record_validation",
4949
"show_stats",
5050
"reset_stats",
51+
"get_stats"
5152
]

src/postal_regex/analytics.py

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -47,50 +47,51 @@ def reset_stats():
4747
else:
4848
print("No statistics file found to reset.")
4949

50+
def get_stats() -> dict:
51+
"""
52+
Loads and returns the validation statistics from the stats file.
53+
This function only retrieves data and does not print anything.
54+
55+
Returns:
56+
dict: A dictionary containing the validation stats, or an empty dict if none exist.
57+
"""
58+
if not STATS_FILE.exists():
59+
return {}
60+
try:
61+
with open(STATS_FILE, "r") as f:
62+
return json.load(f)
63+
except (json.JSONDecodeError, FileNotFoundError):
64+
return {}
65+
5066
def show_stats():
51-
"""Display the validation statistics dashboard in the console."""
52-
stats = _load_stats()
67+
"""
68+
Loads and prints a formatted dashboard of the validation statistics.
69+
This function handles all presentation logic.
70+
"""
71+
stats = get_stats()
5372
if not stats:
54-
print("No validation statistics recorded yet.")
73+
print("No validation analytics recorded yet.")
5574
return
5675

57-
# Prepare data for the table
58-
table_data = []
59-
for country, counts in stats.items():
60-
valid = counts.get("valid", 0)
61-
invalid = counts.get("invalid", 0)
76+
print("--- Postal Regex Validation Stats ---")
77+
78+
sorted_countries = sorted(stats.keys())
79+
80+
print(f"{'Country':<10} | {'Valid':<10} | {'Invalid':<10} | {'Total':<10}")
81+
print("-" * 50)
82+
83+
total_valid = 0
84+
total_invalid = 0
85+
86+
for country in sorted_countries:
87+
valid = stats[country].get("valid", 0)
88+
invalid = stats[country].get("invalid", 0)
6289
total = valid + invalid
63-
table_data.append([country, valid, invalid, total])
64-
65-
# Sort by total validations
66-
table_data.sort(key=lambda row: row[3], reverse=True)
67-
68-
# --- Print Table ---
69-
print("\nPostal Code Validation Stats (Local Project)")
70-
# A simple formatted table without external dependencies
71-
header = ["Country", "Valid", "Invalid", "Total"]
72-
col_widths = [len(h) for h in header]
73-
for row in table_data:
74-
for i, cell in enumerate(row):
75-
col_widths[i] = max(col_widths[i], len(str(cell)))
76-
77-
header_line = " | ".join(h.ljust(w) for h, w in zip(header, col_widths))
78-
separator = "-+-".join("-" * w for w in col_widths)
79-
print(header_line)
80-
print(separator)
81-
82-
for row in table_data:
83-
row_line = " | ".join(str(cell).ljust(w) for cell, w in zip(row, col_widths))
84-
print(row_line)
85-
86-
# --- Print Bar Chart ---
87-
print("\nValidation Success Rate")
88-
for country, valid, invalid, total in table_data:
89-
if total == 0:
90-
success_rate = 0
91-
else:
92-
success_rate = (valid / total) * 100
93-
bar_length = 40
94-
filled_length = int(bar_length * success_rate / 100)
95-
bar = "█" * filled_length + " " * (bar_length - filled_length)
96-
print(f"{country.ljust(5)} |{bar}| {success_rate:.0f}% valid")
90+
total_valid += valid
91+
total_invalid += invalid
92+
print(f"{country:<10} | {valid:<10} | {invalid:<10} | {total:<10}")
93+
94+
print("-" * 50)
95+
grand_total = total_valid + total_invalid
96+
print(f"{'TOTAL':<10} | {total_valid:<10} | {total_invalid:<10} | {grand_total:<10}")
97+
print("-" * 50)

src/postal_regex/core.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,6 @@ def validate(country_identifier: str, postal_code: str, timeout: float = 0.1) ->
7373
is_valid = bool(entry.regex.fullmatch(postal_code, timeout=timeout))
7474
except (regex.TimeoutError, ValueError):
7575
is_valid = False
76-
# --- Record the validation attempt ---
77-
try:
78-
country_code = normalize(country_identifier)
79-
analytics.record_validation(country_code, is_valid)
80-
except ValueError:
81-
pass
8276

8377
return is_valid
8478

tests/test_analytics.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ def test_record_validation_and_load_stats(monkeypatch):
4646

4747
temp_stats_file.unlink()
4848

49+
def test_get_stats(monkeypatch):
50+
"""
51+
Test that get_stats correctly reads and returns data.
52+
"""
53+
temp_stats_file = Path.home() / ".postalregex_stats_temp.json"
54+
monkeypatch.setattr(analytics, 'STATS_FILE', temp_stats_file)
55+
56+
# 1. Test when file doesn't exist
57+
if temp_stats_file.exists():
58+
temp_stats_file.unlink()
59+
assert analytics.get_stats() == {}
60+
61+
dummy_data = {"US": {"valid": 5, "invalid": 1}}
62+
with open(temp_stats_file, "w") as f:
63+
json.dump(dummy_data, f)
64+
65+
assert analytics.get_stats() == dummy_data
66+
67+
temp_stats_file.unlink()
4968

5069
def test_reset_stats(monkeypatch):
5170
"""

0 commit comments

Comments
 (0)