bharatutils 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.
- bharatutils-0.1.0/LICENSE +21 -0
- bharatutils-0.1.0/PKG-INFO +109 -0
- bharatutils-0.1.0/README.md +85 -0
- bharatutils-0.1.0/bharatutils/__init__.py +9 -0
- bharatutils-0.1.0/bharatutils/address.py +78 -0
- bharatutils-0.1.0/bharatutils/festivals.py +74 -0
- bharatutils-0.1.0/bharatutils/gst.py +71 -0
- bharatutils-0.1.0/bharatutils/number_format.py +38 -0
- bharatutils-0.1.0/bharatutils/pan.py +40 -0
- bharatutils-0.1.0/bharatutils.egg-info/PKG-INFO +109 -0
- bharatutils-0.1.0/bharatutils.egg-info/SOURCES.txt +13 -0
- bharatutils-0.1.0/bharatutils.egg-info/dependency_links.txt +1 -0
- bharatutils-0.1.0/bharatutils.egg-info/top_level.txt +1 -0
- bharatutils-0.1.0/pyproject.toml +34 -0
- bharatutils-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ansuman Jaiswal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bharatutils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python utilities for Indian developers — lakh/crore formatting, GST & PAN validation, address parsing, festival calendar
|
|
5
|
+
Author: Ansuman Jaiswal
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iam-ansuman/bharatutils
|
|
8
|
+
Project-URL: Issues, https://github.com/iam-ansuman/bharatutils/issues
|
|
9
|
+
Keywords: india,gst,pan,lakh,crore,inr,pincode,address,festivals,indian
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# bharatutils 🇮🇳
|
|
26
|
+
|
|
27
|
+
Python utilities for Indian developers — because `1,500,000` should display as `₹15 L`.
|
|
28
|
+
|
|
29
|
+
Lakh/crore formatting · GST & PAN validation · Address parsing · Festival calendar
|
|
30
|
+
|
|
31
|
+
## Why?
|
|
32
|
+
|
|
33
|
+
Every Indian developer has written these same utility functions a hundred times:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# Without bharatutils 😩
|
|
37
|
+
df["salary_display"] = df["salary"].apply(
|
|
38
|
+
lambda x: f"₹{round(x/100000, 2)} L" if x >= 100000 else f"₹{x:,}"
|
|
39
|
+
) # ...and it crashes on NaN
|
|
40
|
+
|
|
41
|
+
# With bharatutils 😎
|
|
42
|
+
from bharatutils import format_inr
|
|
43
|
+
df["salary_display"] = df["salary"].apply(format_inr) # handles NaN, strings, negatives
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install bharatutils
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
### 💰 Indian number formatting
|
|
55
|
+
```python
|
|
56
|
+
from bharatutils import format_inr, to_lakh, to_crore
|
|
57
|
+
|
|
58
|
+
format_inr(1500000) # '₹15.0 L'
|
|
59
|
+
format_inr(50000000) # '₹5.0 Cr'
|
|
60
|
+
format_inr("15,00,000") # '₹15.0 L' — handles messy strings
|
|
61
|
+
to_lakh(1500000) # 15.0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 🧾 GST validation — with real checksum
|
|
65
|
+
```python
|
|
66
|
+
from bharatutils import validate_gstin_strict, parse_gstin
|
|
67
|
+
|
|
68
|
+
validate_gstin_strict("27AAAPZ2318J1ZI") # True — verifies the check digit
|
|
69
|
+
parse_gstin("27AAAPZ2318J1ZI")
|
|
70
|
+
# {'state': 'Maharashtra', 'pan': 'AAAPZ2318J', 'entity_number': '1', ...}
|
|
71
|
+
```
|
|
72
|
+
Catches single-character typos that format-only validators miss.
|
|
73
|
+
|
|
74
|
+
### 🪪 PAN validation + holder type
|
|
75
|
+
```python
|
|
76
|
+
from bharatutils import parse_pan
|
|
77
|
+
|
|
78
|
+
parse_pan("AAAPZ2318J")
|
|
79
|
+
# {'holder_type': 'Individual', 'is_individual': True, ...}
|
|
80
|
+
# P=Person, C=Company, T=Trust, G=Government...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 📍 Indian address parsing
|
|
84
|
+
```python
|
|
85
|
+
from bharatutils import parse_address
|
|
86
|
+
|
|
87
|
+
parse_address("Flat 302, Nr. SBI ATM, MG Road, Pune - 411001")
|
|
88
|
+
# {'pincode': '411001', 'state': 'Maharashtra', 'city': 'Pune'}
|
|
89
|
+
```
|
|
90
|
+
Handles space-broken pincodes ("700 016"), ignores phone numbers, never crashes.
|
|
91
|
+
|
|
92
|
+
### 🪔 Festival calendar
|
|
93
|
+
```python
|
|
94
|
+
from bharatutils import next_festival, days_until
|
|
95
|
+
|
|
96
|
+
next_festival() # {'name': 'Muharram', 'date': datetime.date(2026, 6, 26)}
|
|
97
|
+
days_until("Diwali") # 150
|
|
98
|
+
```
|
|
99
|
+
Covers Hindu, Muslim, Christian, Sikh & Jain festivals plus national holidays — verified against official Government of India lists.
|
|
100
|
+
|
|
101
|
+
## Status
|
|
102
|
+
|
|
103
|
+
`v0.1.0` — early but tested. Pincode→state mapping is prefix-based (~95% accurate); exact-lookup coming in v0.2.
|
|
104
|
+
|
|
105
|
+
Found a bug? [Open an issue](../../issues) — responses are fast.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# bharatutils 🇮🇳
|
|
2
|
+
|
|
3
|
+
Python utilities for Indian developers — because `1,500,000` should display as `₹15 L`.
|
|
4
|
+
|
|
5
|
+
Lakh/crore formatting · GST & PAN validation · Address parsing · Festival calendar
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
Every Indian developer has written these same utility functions a hundred times:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
# Without bharatutils 😩
|
|
13
|
+
df["salary_display"] = df["salary"].apply(
|
|
14
|
+
lambda x: f"₹{round(x/100000, 2)} L" if x >= 100000 else f"₹{x:,}"
|
|
15
|
+
) # ...and it crashes on NaN
|
|
16
|
+
|
|
17
|
+
# With bharatutils 😎
|
|
18
|
+
from bharatutils import format_inr
|
|
19
|
+
df["salary_display"] = df["salary"].apply(format_inr) # handles NaN, strings, negatives
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install bharatutils
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
### 💰 Indian number formatting
|
|
31
|
+
```python
|
|
32
|
+
from bharatutils import format_inr, to_lakh, to_crore
|
|
33
|
+
|
|
34
|
+
format_inr(1500000) # '₹15.0 L'
|
|
35
|
+
format_inr(50000000) # '₹5.0 Cr'
|
|
36
|
+
format_inr("15,00,000") # '₹15.0 L' — handles messy strings
|
|
37
|
+
to_lakh(1500000) # 15.0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 🧾 GST validation — with real checksum
|
|
41
|
+
```python
|
|
42
|
+
from bharatutils import validate_gstin_strict, parse_gstin
|
|
43
|
+
|
|
44
|
+
validate_gstin_strict("27AAAPZ2318J1ZI") # True — verifies the check digit
|
|
45
|
+
parse_gstin("27AAAPZ2318J1ZI")
|
|
46
|
+
# {'state': 'Maharashtra', 'pan': 'AAAPZ2318J', 'entity_number': '1', ...}
|
|
47
|
+
```
|
|
48
|
+
Catches single-character typos that format-only validators miss.
|
|
49
|
+
|
|
50
|
+
### 🪪 PAN validation + holder type
|
|
51
|
+
```python
|
|
52
|
+
from bharatutils import parse_pan
|
|
53
|
+
|
|
54
|
+
parse_pan("AAAPZ2318J")
|
|
55
|
+
# {'holder_type': 'Individual', 'is_individual': True, ...}
|
|
56
|
+
# P=Person, C=Company, T=Trust, G=Government...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 📍 Indian address parsing
|
|
60
|
+
```python
|
|
61
|
+
from bharatutils import parse_address
|
|
62
|
+
|
|
63
|
+
parse_address("Flat 302, Nr. SBI ATM, MG Road, Pune - 411001")
|
|
64
|
+
# {'pincode': '411001', 'state': 'Maharashtra', 'city': 'Pune'}
|
|
65
|
+
```
|
|
66
|
+
Handles space-broken pincodes ("700 016"), ignores phone numbers, never crashes.
|
|
67
|
+
|
|
68
|
+
### 🪔 Festival calendar
|
|
69
|
+
```python
|
|
70
|
+
from bharatutils import next_festival, days_until
|
|
71
|
+
|
|
72
|
+
next_festival() # {'name': 'Muharram', 'date': datetime.date(2026, 6, 26)}
|
|
73
|
+
days_until("Diwali") # 150
|
|
74
|
+
```
|
|
75
|
+
Covers Hindu, Muslim, Christian, Sikh & Jain festivals plus national holidays — verified against official Government of India lists.
|
|
76
|
+
|
|
77
|
+
## Status
|
|
78
|
+
|
|
79
|
+
`v0.1.0` — early but tested. Pincode→state mapping is prefix-based (~95% accurate); exact-lookup coming in v0.2.
|
|
80
|
+
|
|
81
|
+
Found a bug? [Open an issue](../../issues) — responses are fast.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""bharatutils — Python utilities for Indian developers."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .number_format import to_lakh, to_crore, format_inr
|
|
6
|
+
from .gst import validate_gstin, validate_gstin_strict, parse_gstin
|
|
7
|
+
from .pan import validate_pan, parse_pan
|
|
8
|
+
from .address import parse_address, extract_pincode, pincode_to_state
|
|
9
|
+
from .festivals import get_festivals, next_festival, days_until, is_festival
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
PINCODE_PATTERN = re.compile(r"\b[1-9][0-9]{5}\b")
|
|
5
|
+
|
|
6
|
+
PINCODE_PREFIX_TO_STATE = {
|
|
7
|
+
"11": "Delhi",
|
|
8
|
+
"12": "Haryana", "13": "Haryana",
|
|
9
|
+
"14": "Punjab", "15": "Punjab",
|
|
10
|
+
"16": "Chandigarh",
|
|
11
|
+
"17": "Himachal Pradesh",
|
|
12
|
+
"18": "Jammu & Kashmir", "19": "Jammu & Kashmir",
|
|
13
|
+
"20": "Uttar Pradesh", "21": "Uttar Pradesh", "22": "Uttar Pradesh",
|
|
14
|
+
"23": "Uttar Pradesh", "24": "Uttar Pradesh", "25": "Uttar Pradesh",
|
|
15
|
+
"26": "Uttar Pradesh", "27": "Uttar Pradesh", "28": "Uttar Pradesh",
|
|
16
|
+
"30": "Rajasthan", "31": "Rajasthan", "32": "Rajasthan", "33": "Rajasthan", "34": "Rajasthan",
|
|
17
|
+
"36": "Gujarat", "37": "Gujarat", "38": "Gujarat", "39": "Gujarat",
|
|
18
|
+
"40": "Maharashtra", "41": "Maharashtra", "42": "Maharashtra",
|
|
19
|
+
"43": "Maharashtra", "44": "Maharashtra",
|
|
20
|
+
"45": "Madhya Pradesh", "46": "Madhya Pradesh", "47": "Madhya Pradesh", "48": "Madhya Pradesh",
|
|
21
|
+
"49": "Chhattisgarh",
|
|
22
|
+
"50": "Telangana",
|
|
23
|
+
"51": "Andhra Pradesh", "52": "Andhra Pradesh", "53": "Andhra Pradesh",
|
|
24
|
+
"56": "Karnataka", "57": "Karnataka", "58": "Karnataka", "59": "Karnataka",
|
|
25
|
+
"60": "Tamil Nadu", "61": "Tamil Nadu", "62": "Tamil Nadu", "63": "Tamil Nadu", "64": "Tamil Nadu",
|
|
26
|
+
"67": "Kerala", "68": "Kerala", "69": "Kerala",
|
|
27
|
+
"70": "West Bengal", "71": "West Bengal", "72": "West Bengal", "73": "West Bengal", "74": "West Bengal",
|
|
28
|
+
"75": "Odisha", "76": "Odisha", "77": "Odisha",
|
|
29
|
+
"78": "Assam",
|
|
30
|
+
"79": "North East",
|
|
31
|
+
"80": "Bihar", "81": "Bihar", "82": "Bihar", "83": "Bihar", "84": "Bihar", "85": "Bihar",
|
|
32
|
+
"90": "Army Postal Service", "91": "Army Postal Service", "92": "Army Postal Service",
|
|
33
|
+
"99": "Army Postal Service",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
KNOWN_CITIES = [
|
|
37
|
+
"Mumbai", "Delhi", "Bangalore", "Bengaluru", "Hyderabad", "Chennai",
|
|
38
|
+
"Kolkata", "Pune", "Ahmedabad", "Jaipur", "Surat", "Lucknow",
|
|
39
|
+
"Kanpur", "Nagpur", "Indore", "Bhopal", "Patna", "Vadodara",
|
|
40
|
+
"Ludhiana", "Agra", "Nashik", "Varanasi", "Amritsar", "Noida",
|
|
41
|
+
"Gurgaon", "Gurugram", "Chandigarh", "Coimbatore", "Kochi", "Goa",
|
|
42
|
+
"Visakhapatnam", "Thane", "Guwahati", "Bhubaneswar", "Mysore", "Mysuru",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_pincode(address):
|
|
47
|
+
"""Find the pincode in a messy address string. None if not found."""
|
|
48
|
+
if address is None or not isinstance(address, str):
|
|
49
|
+
return None
|
|
50
|
+
cleaned = re.sub(r"(\b[1-9][0-9]{2})\s+([0-9]{3}\b)", r"\1\2", address)
|
|
51
|
+
match = PINCODE_PATTERN.search(cleaned)
|
|
52
|
+
return match.group() if match else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pincode_to_state(pincode):
|
|
56
|
+
"""Get the state from a pincode. None if unknown."""
|
|
57
|
+
if pincode is None:
|
|
58
|
+
return None
|
|
59
|
+
pincode = str(pincode).strip()
|
|
60
|
+
if not re.match(r"^[1-9][0-9]{5}$", pincode):
|
|
61
|
+
return None
|
|
62
|
+
return PINCODE_PREFIX_TO_STATE.get(pincode[:2])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_address(address):
|
|
66
|
+
"""Parse a messy Indian address. Always returns a dict."""
|
|
67
|
+
result = {"raw": address, "pincode": None, "state": None, "city": None}
|
|
68
|
+
if address is None or not isinstance(address, str):
|
|
69
|
+
return result
|
|
70
|
+
result["pincode"] = extract_pincode(address)
|
|
71
|
+
if result["pincode"]:
|
|
72
|
+
result["state"] = pincode_to_state(result["pincode"])
|
|
73
|
+
address_lower = address.lower()
|
|
74
|
+
for city in KNOWN_CITIES:
|
|
75
|
+
if re.search(r"\b" + city.lower() + r"\b", address_lower):
|
|
76
|
+
result["city"] = city
|
|
77
|
+
break
|
|
78
|
+
return result
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
FIXED_FESTIVALS = {
|
|
5
|
+
"New Year": (1, 1),
|
|
6
|
+
"Republic Day": (1, 26),
|
|
7
|
+
"Independence Day": (8, 15),
|
|
8
|
+
"Gandhi Jayanti": (10, 2),
|
|
9
|
+
"Christmas": (12, 25),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Verified against official Government of India holiday lists
|
|
13
|
+
MOVABLE_FESTIVALS = {
|
|
14
|
+
2026: {
|
|
15
|
+
"Makar Sankranti": (1, 14),
|
|
16
|
+
"Holi": (3, 4),
|
|
17
|
+
"Eid ul-Fitr": (3, 21),
|
|
18
|
+
"Ram Navami": (3, 26),
|
|
19
|
+
"Mahavir Jayanti": (3, 31),
|
|
20
|
+
"Good Friday": (4, 3),
|
|
21
|
+
"Eid ul-Adha": (5, 27),
|
|
22
|
+
"Muharram": (6, 26),
|
|
23
|
+
"Milad-un-Nabi": (8, 26),
|
|
24
|
+
"Janmashtami": (9, 4),
|
|
25
|
+
"Ganesh Chaturthi": (9, 14),
|
|
26
|
+
"Dussehra": (10, 20),
|
|
27
|
+
"Diwali": (11, 8),
|
|
28
|
+
"Guru Nanak Jayanti": (11, 24),
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_festivals(year):
|
|
34
|
+
"""All festivals for a year, sorted by date."""
|
|
35
|
+
festivals = []
|
|
36
|
+
for name, (m, d) in FIXED_FESTIVALS.items():
|
|
37
|
+
festivals.append({"name": name, "date": date(year, m, d), "type": "fixed"})
|
|
38
|
+
if year in MOVABLE_FESTIVALS:
|
|
39
|
+
for name, (m, d) in MOVABLE_FESTIVALS[year].items():
|
|
40
|
+
festivals.append({"name": name, "date": date(year, m, d), "type": "movable"})
|
|
41
|
+
festivals.sort(key=lambda f: f["date"])
|
|
42
|
+
return festivals
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def next_festival(from_date=None):
|
|
46
|
+
"""The next upcoming festival from a given date (default: today)."""
|
|
47
|
+
if from_date is None:
|
|
48
|
+
from_date = date.today()
|
|
49
|
+
for year in [from_date.year, from_date.year + 1]:
|
|
50
|
+
for f in get_festivals(year):
|
|
51
|
+
if f["date"] >= from_date:
|
|
52
|
+
return f
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def days_until(festival_name, from_date=None):
|
|
57
|
+
"""Days remaining until a festival. None if not found."""
|
|
58
|
+
if from_date is None:
|
|
59
|
+
from_date = date.today()
|
|
60
|
+
for year in [from_date.year, from_date.year + 1]:
|
|
61
|
+
for f in get_festivals(year):
|
|
62
|
+
if f["name"].lower() == festival_name.lower() and f["date"] >= from_date:
|
|
63
|
+
return (f["date"] - from_date).days
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_festival(check_date=None):
|
|
68
|
+
"""Is this date a festival? Returns the festival name or None."""
|
|
69
|
+
if check_date is None:
|
|
70
|
+
check_date = date.today()
|
|
71
|
+
for f in get_festivals(check_date.year):
|
|
72
|
+
if f["date"] == check_date:
|
|
73
|
+
return f["name"]
|
|
74
|
+
return None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
STATE_CODES = {
|
|
5
|
+
"01": "Jammu & Kashmir", "02": "Himachal Pradesh",
|
|
6
|
+
"03": "Punjab", "04": "Chandigarh",
|
|
7
|
+
"05": "Uttarakhand", "06": "Haryana",
|
|
8
|
+
"07": "Delhi", "08": "Rajasthan",
|
|
9
|
+
"09": "Uttar Pradesh", "10": "Bihar",
|
|
10
|
+
"11": "Sikkim", "12": "Arunachal Pradesh",
|
|
11
|
+
"13": "Nagaland", "14": "Manipur",
|
|
12
|
+
"15": "Mizoram", "16": "Tripura",
|
|
13
|
+
"17": "Meghalaya", "18": "Assam",
|
|
14
|
+
"19": "West Bengal", "20": "Jharkhand",
|
|
15
|
+
"21": "Odisha", "22": "Chhattisgarh",
|
|
16
|
+
"23": "Madhya Pradesh", "24": "Gujarat",
|
|
17
|
+
"25": "Daman & Diu", "26": "Dadra & Nagar Haveli",
|
|
18
|
+
"27": "Maharashtra", "28": "Andhra Pradesh",
|
|
19
|
+
"29": "Karnataka", "30": "Goa",
|
|
20
|
+
"31": "Lakshadweep", "32": "Kerala",
|
|
21
|
+
"33": "Tamil Nadu", "34": "Puducherry",
|
|
22
|
+
"35": "Andaman & Nicobar","36": "Telangana",
|
|
23
|
+
"37": "Andhra Pradesh (New)",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
GSTIN_PATTERN = re.compile(r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_gstin(gstin):
|
|
30
|
+
"""Check if a GSTIN has valid format and a real state code."""
|
|
31
|
+
if gstin is None or not isinstance(gstin, str):
|
|
32
|
+
return False
|
|
33
|
+
gstin = gstin.strip().upper()
|
|
34
|
+
if len(gstin) != 15 or not GSTIN_PATTERN.match(gstin):
|
|
35
|
+
return False
|
|
36
|
+
return gstin[:2] in STATE_CODES
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_gstin(gstin):
|
|
40
|
+
"""Break a GSTIN into state, PAN, entity number. None if invalid."""
|
|
41
|
+
if not validate_gstin(gstin):
|
|
42
|
+
return None
|
|
43
|
+
gstin = gstin.strip().upper()
|
|
44
|
+
return {
|
|
45
|
+
"gstin": gstin,
|
|
46
|
+
"state_code": gstin[:2],
|
|
47
|
+
"state": STATE_CODES[gstin[:2]],
|
|
48
|
+
"pan": gstin[2:12],
|
|
49
|
+
"entity_number": gstin[12],
|
|
50
|
+
"check_digit": gstin[14],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def gstin_check_digit(gstin_first_14):
|
|
55
|
+
"""Official mod-36 checksum for GSTIN."""
|
|
56
|
+
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
57
|
+
total = 0
|
|
58
|
+
for i, char in enumerate(gstin_first_14):
|
|
59
|
+
value = chars.index(char)
|
|
60
|
+
factor = 2 if i % 2 != 0 else 1
|
|
61
|
+
product = value * factor
|
|
62
|
+
total += product // 36 + product % 36
|
|
63
|
+
return chars[(36 - total % 36) % 36]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate_gstin_strict(gstin):
|
|
67
|
+
"""Full validation: format + state + checksum."""
|
|
68
|
+
if not validate_gstin(gstin):
|
|
69
|
+
return False
|
|
70
|
+
gstin = gstin.strip().upper()
|
|
71
|
+
return gstin_check_digit(gstin[:14]) == gstin[14]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
def to_lakh(number):
|
|
3
|
+
"""Convert a number to lakhs. Handles strings, negatives, None, NaN."""
|
|
4
|
+
if number is None or (isinstance(number, float) and number != number):
|
|
5
|
+
return None
|
|
6
|
+
if isinstance(number, str):
|
|
7
|
+
number = float(number.replace(",", "").replace("₹", "").strip())
|
|
8
|
+
return round(number / 100_000, 2)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_crore(number):
|
|
12
|
+
"""Convert a number to crores. Handles strings, negatives, None, NaN."""
|
|
13
|
+
if number is None or (isinstance(number, float) and number != number):
|
|
14
|
+
return None
|
|
15
|
+
if isinstance(number, str):
|
|
16
|
+
number = float(number.replace(",", "").replace("₹", "").strip())
|
|
17
|
+
return round(number / 10_000_000, 2)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_inr(number, symbol=True):
|
|
21
|
+
"""
|
|
22
|
+
Format a number in Indian style (lakh/crore).
|
|
23
|
+
symbol=False gives you 15.0 L without the rupee sign.
|
|
24
|
+
"""
|
|
25
|
+
if number is None or (isinstance(number, float) and number != number):
|
|
26
|
+
return "N/A"
|
|
27
|
+
if isinstance(number, str):
|
|
28
|
+
number = float(number.replace(",", "").replace("₹", "").strip())
|
|
29
|
+
prefix = "₹" if symbol else ""
|
|
30
|
+
negative = number < 0
|
|
31
|
+
number = abs(number)
|
|
32
|
+
if number >= 10_000_000:
|
|
33
|
+
result = f"{prefix}{to_crore(number)} Cr"
|
|
34
|
+
elif number >= 100_000:
|
|
35
|
+
result = f"{prefix}{to_lakh(number)} L"
|
|
36
|
+
else:
|
|
37
|
+
result = f"{prefix}{number:,.0f}"
|
|
38
|
+
return f"-{result}" if negative else result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
PAN_PATTERN = re.compile(r"^[A-Z]{5}[0-9]{4}[A-Z]{1}$")
|
|
5
|
+
|
|
6
|
+
HOLDER_TYPES = {
|
|
7
|
+
"P": "Individual",
|
|
8
|
+
"C": "Company",
|
|
9
|
+
"H": "Hindu Undivided Family",
|
|
10
|
+
"F": "Firm / Partnership",
|
|
11
|
+
"T": "Trust",
|
|
12
|
+
"G": "Government",
|
|
13
|
+
"A": "Association of Persons",
|
|
14
|
+
"B": "Body of Individuals",
|
|
15
|
+
"L": "Local Authority",
|
|
16
|
+
"J": "Artificial Juridical Person",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_pan(pan):
|
|
21
|
+
"""Check if a PAN is valid. Returns True or False."""
|
|
22
|
+
if pan is None or not isinstance(pan, str):
|
|
23
|
+
return False
|
|
24
|
+
pan = pan.strip().upper()
|
|
25
|
+
if len(pan) != 10 or not PAN_PATTERN.match(pan):
|
|
26
|
+
return False
|
|
27
|
+
return pan[3] in HOLDER_TYPES
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_pan(pan):
|
|
31
|
+
"""Extract holder type and name initial from a PAN."""
|
|
32
|
+
if not validate_pan(pan):
|
|
33
|
+
return None
|
|
34
|
+
pan = pan.strip().upper()
|
|
35
|
+
return {
|
|
36
|
+
"pan": pan,
|
|
37
|
+
"holder_type": HOLDER_TYPES[pan[3]],
|
|
38
|
+
"name_initial": pan[4],
|
|
39
|
+
"is_individual": pan[3] == "P",
|
|
40
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bharatutils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python utilities for Indian developers — lakh/crore formatting, GST & PAN validation, address parsing, festival calendar
|
|
5
|
+
Author: Ansuman Jaiswal
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iam-ansuman/bharatutils
|
|
8
|
+
Project-URL: Issues, https://github.com/iam-ansuman/bharatutils/issues
|
|
9
|
+
Keywords: india,gst,pan,lakh,crore,inr,pincode,address,festivals,indian
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# bharatutils 🇮🇳
|
|
26
|
+
|
|
27
|
+
Python utilities for Indian developers — because `1,500,000` should display as `₹15 L`.
|
|
28
|
+
|
|
29
|
+
Lakh/crore formatting · GST & PAN validation · Address parsing · Festival calendar
|
|
30
|
+
|
|
31
|
+
## Why?
|
|
32
|
+
|
|
33
|
+
Every Indian developer has written these same utility functions a hundred times:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# Without bharatutils 😩
|
|
37
|
+
df["salary_display"] = df["salary"].apply(
|
|
38
|
+
lambda x: f"₹{round(x/100000, 2)} L" if x >= 100000 else f"₹{x:,}"
|
|
39
|
+
) # ...and it crashes on NaN
|
|
40
|
+
|
|
41
|
+
# With bharatutils 😎
|
|
42
|
+
from bharatutils import format_inr
|
|
43
|
+
df["salary_display"] = df["salary"].apply(format_inr) # handles NaN, strings, negatives
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install bharatutils
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
### 💰 Indian number formatting
|
|
55
|
+
```python
|
|
56
|
+
from bharatutils import format_inr, to_lakh, to_crore
|
|
57
|
+
|
|
58
|
+
format_inr(1500000) # '₹15.0 L'
|
|
59
|
+
format_inr(50000000) # '₹5.0 Cr'
|
|
60
|
+
format_inr("15,00,000") # '₹15.0 L' — handles messy strings
|
|
61
|
+
to_lakh(1500000) # 15.0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 🧾 GST validation — with real checksum
|
|
65
|
+
```python
|
|
66
|
+
from bharatutils import validate_gstin_strict, parse_gstin
|
|
67
|
+
|
|
68
|
+
validate_gstin_strict("27AAAPZ2318J1ZI") # True — verifies the check digit
|
|
69
|
+
parse_gstin("27AAAPZ2318J1ZI")
|
|
70
|
+
# {'state': 'Maharashtra', 'pan': 'AAAPZ2318J', 'entity_number': '1', ...}
|
|
71
|
+
```
|
|
72
|
+
Catches single-character typos that format-only validators miss.
|
|
73
|
+
|
|
74
|
+
### 🪪 PAN validation + holder type
|
|
75
|
+
```python
|
|
76
|
+
from bharatutils import parse_pan
|
|
77
|
+
|
|
78
|
+
parse_pan("AAAPZ2318J")
|
|
79
|
+
# {'holder_type': 'Individual', 'is_individual': True, ...}
|
|
80
|
+
# P=Person, C=Company, T=Trust, G=Government...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 📍 Indian address parsing
|
|
84
|
+
```python
|
|
85
|
+
from bharatutils import parse_address
|
|
86
|
+
|
|
87
|
+
parse_address("Flat 302, Nr. SBI ATM, MG Road, Pune - 411001")
|
|
88
|
+
# {'pincode': '411001', 'state': 'Maharashtra', 'city': 'Pune'}
|
|
89
|
+
```
|
|
90
|
+
Handles space-broken pincodes ("700 016"), ignores phone numbers, never crashes.
|
|
91
|
+
|
|
92
|
+
### 🪔 Festival calendar
|
|
93
|
+
```python
|
|
94
|
+
from bharatutils import next_festival, days_until
|
|
95
|
+
|
|
96
|
+
next_festival() # {'name': 'Muharram', 'date': datetime.date(2026, 6, 26)}
|
|
97
|
+
days_until("Diwali") # 150
|
|
98
|
+
```
|
|
99
|
+
Covers Hindu, Muslim, Christian, Sikh & Jain festivals plus national holidays — verified against official Government of India lists.
|
|
100
|
+
|
|
101
|
+
## Status
|
|
102
|
+
|
|
103
|
+
`v0.1.0` — early but tested. Pincode→state mapping is prefix-based (~95% accurate); exact-lookup coming in v0.2.
|
|
104
|
+
|
|
105
|
+
Found a bug? [Open an issue](../../issues) — responses are fast.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
bharatutils/__init__.py
|
|
5
|
+
bharatutils/address.py
|
|
6
|
+
bharatutils/festivals.py
|
|
7
|
+
bharatutils/gst.py
|
|
8
|
+
bharatutils/number_format.py
|
|
9
|
+
bharatutils/pan.py
|
|
10
|
+
bharatutils.egg-info/PKG-INFO
|
|
11
|
+
bharatutils.egg-info/SOURCES.txt
|
|
12
|
+
bharatutils.egg-info/dependency_links.txt
|
|
13
|
+
bharatutils.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bharatutils
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bharatutils"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python utilities for Indian developers — lakh/crore formatting, GST & PAN validation, address parsing, festival calendar"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Ansuman Jaiswal"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["india", "gst", "pan", "lakh", "crore", "inr", "pincode", "address", "festivals", "indian"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
requires-python = ">=3.8"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/iam-ansuman/bharatutils"
|
|
31
|
+
Issues = "https://github.com/iam-ansuman/bharatutils/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["bharatutils"]
|