egyid 1.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.
- egyid-1.0.0/LICENSE +21 -0
- egyid-1.0.0/PKG-INFO +211 -0
- egyid-1.0.0/README.md +167 -0
- egyid-1.0.0/egyid/__init__.py +3 -0
- egyid-1.0.0/egyid/generator.py +46 -0
- egyid-1.0.0/egyid/governorates.py +57 -0
- egyid-1.0.0/egyid/id.py +150 -0
- egyid-1.0.0/egyid/utils.py +13 -0
- egyid-1.0.0/egyid/validation.py +55 -0
- egyid-1.0.0/egyid.egg-info/PKG-INFO +211 -0
- egyid-1.0.0/egyid.egg-info/SOURCES.txt +14 -0
- egyid-1.0.0/egyid.egg-info/dependency_links.txt +1 -0
- egyid-1.0.0/egyid.egg-info/top_level.txt +1 -0
- egyid-1.0.0/pyproject.toml +39 -0
- egyid-1.0.0/setup.cfg +4 -0
- egyid-1.0.0/tests/test_egyptian_national_id.py +218 -0
egyid-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Petter0x1
|
|
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.
|
egyid-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: egyid
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python library for parsing, validating, and generating Egyptian National IDs
|
|
5
|
+
Author: Petter0x1
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Petter0x1
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/Petter0x1/egyid
|
|
28
|
+
Project-URL: Repository, https://github.com/Petter0x1/egyid
|
|
29
|
+
Project-URL: Issues, https://github.com/Petter0x1/egyid/issues
|
|
30
|
+
Keywords: egypt,national-id,validation,parsing,generation
|
|
31
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# Egyptian National ID
|
|
46
|
+
|
|
47
|
+
[](https://pypi.org/project/egyid/)
|
|
48
|
+
[](https://pypi.org/project/egyid/)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
[](https://github.com/Petter0x1/egyid/actions)
|
|
51
|
+
|
|
52
|
+
A lightweight and comprehensive Python library for validating, parsing, and generating Egyptian National ID numbers. Built with type hints and designed for reliability in production environments.
|
|
53
|
+
|
|
54
|
+
## Table of Contents
|
|
55
|
+
|
|
56
|
+
- [Features](#features)
|
|
57
|
+
- [Installation](#installation)
|
|
58
|
+
- [Quick Start](#quick-start)
|
|
59
|
+
- [API Reference](#api-reference)
|
|
60
|
+
- [Testing](#testing)
|
|
61
|
+
- [Contributing](#contributing)
|
|
62
|
+
- [License](#license)
|
|
63
|
+
- [Author](#author)
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
- **Validation**: Comprehensive ID validation including checksum verification, date validation, and governorate checks
|
|
68
|
+
- **Parsing**: Extract birth date, gender, age, governorate, and region information from ID numbers
|
|
69
|
+
- **Generation**: Create valid mock Egyptian National IDs for testing and development
|
|
70
|
+
- **Type Safety**: Full type hints for better IDE support and code reliability
|
|
71
|
+
- **Arabic Support**: Handles Arabic numerals in ID input
|
|
72
|
+
- **Cross-Platform**: Compatible with Python 3.8+
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
### From PyPI (Recommended)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install egyid
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### From Source
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/Petter0x1/egyid.git
|
|
86
|
+
cd egyid
|
|
87
|
+
pip install .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Requirements
|
|
91
|
+
|
|
92
|
+
- Python 3.8 or higher
|
|
93
|
+
- No external dependencies
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from egyid import EgyptianNationalId
|
|
99
|
+
|
|
100
|
+
# Validate an ID
|
|
101
|
+
is_valid = EgyptianNationalId.is_valid_id("29010280165195")
|
|
102
|
+
print(is_valid) # True
|
|
103
|
+
|
|
104
|
+
# Parse an ID
|
|
105
|
+
eid = EgyptianNationalId("29010280165195")
|
|
106
|
+
if eid.is_valid():
|
|
107
|
+
print(f"Birth Date: {eid.birth_date}") # 1990-10-28
|
|
108
|
+
print(f"Gender: {eid.gender}") # male
|
|
109
|
+
print(f"Governorate: {eid.governorate}") # Cairo
|
|
110
|
+
print(f"Age: {eid.age}") # 35 (as of 2026)
|
|
111
|
+
print(f"Region: {eid.region_name}") # Cairo
|
|
112
|
+
|
|
113
|
+
# Generate a new ID
|
|
114
|
+
new_id = EgyptianNationalId.generate({
|
|
115
|
+
"year": 1995,
|
|
116
|
+
"gender": "female",
|
|
117
|
+
"governorate": "01" # Cairo
|
|
118
|
+
})
|
|
119
|
+
print(new_id) # e.g., "39501010100000" (with correct checksum)
|
|
120
|
+
|
|
121
|
+
# Get full information as dictionary
|
|
122
|
+
info = eid.to_dict()
|
|
123
|
+
print(info)
|
|
124
|
+
# {
|
|
125
|
+
# "nationalId": "29010280165195",
|
|
126
|
+
# "birthYear": 1990,
|
|
127
|
+
# "birthMonth": 10,
|
|
128
|
+
# "birthDay": 28,
|
|
129
|
+
# "age": 35,
|
|
130
|
+
# "gender": "male",
|
|
131
|
+
# "governorate": "Cairo",
|
|
132
|
+
# "region": "Cairo",
|
|
133
|
+
# "insideEgypt": true,
|
|
134
|
+
# "isAdult": true
|
|
135
|
+
# }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API Reference
|
|
139
|
+
|
|
140
|
+
### Class: `EgyptianNationalId`
|
|
141
|
+
|
|
142
|
+
#### Methods
|
|
143
|
+
|
|
144
|
+
- `EgyptianNationalId(id_value: Union[str, int])` - Create an instance from an ID string or integer
|
|
145
|
+
- `is_valid() -> bool` - Check if the ID is valid
|
|
146
|
+
- `to_dict() -> Optional[Dict[str, Any]]` - Return ID information as a dictionary (None if invalid)
|
|
147
|
+
|
|
148
|
+
#### Properties
|
|
149
|
+
|
|
150
|
+
- `birth_date: Optional[date]` - Birth date
|
|
151
|
+
- `age: Optional[int]` - Current age
|
|
152
|
+
- `gender: str` - 'male' or 'female'
|
|
153
|
+
- `governorate: Optional[str]` - Governorate name
|
|
154
|
+
- `region_name: str` - Region name
|
|
155
|
+
|
|
156
|
+
#### Class Methods
|
|
157
|
+
|
|
158
|
+
- `is_valid_id(id_value: Union[str, int]) -> bool` - Validate without creating instance
|
|
159
|
+
- `generate(options: Optional[Dict[str, Any]] = None) -> str` - Generate a new valid ID
|
|
160
|
+
- `parse(id_value: Union[str, int]) -> EgyptianNationalId` - Alias for constructor
|
|
161
|
+
- `check_is_male(id_value: Union[str, int]) -> bool` - Check if ID belongs to male
|
|
162
|
+
- `check_is_female(id_value: Union[str, int]) -> bool` - Check if ID belongs to female
|
|
163
|
+
- `check_is_adult(id_value: Union[str, int]) -> bool` - Check if person is 18+
|
|
164
|
+
|
|
165
|
+
### Generation Options
|
|
166
|
+
|
|
167
|
+
The `generate()` method accepts an optional dictionary with:
|
|
168
|
+
- `year: int` - Birth year (default: random 1950-current)
|
|
169
|
+
- `month: int` - Birth month (default: random 1-12)
|
|
170
|
+
- `day: int` - Birth day (default: valid for month/year)
|
|
171
|
+
- `gender: str` - 'male' or 'female' (default: random)
|
|
172
|
+
- `governorate: str` - 2-digit governorate code (default: random)
|
|
173
|
+
|
|
174
|
+
## Testing
|
|
175
|
+
|
|
176
|
+
Run the test suite:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python -m unittest discover tests
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Tests cover validation, parsing, generation, and edge cases across all supported Python versions.
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
Contributions are welcome! Please:
|
|
187
|
+
|
|
188
|
+
1. Fork the repository
|
|
189
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
190
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
191
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
192
|
+
5. Open a Pull Request
|
|
193
|
+
|
|
194
|
+
### Development Setup
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git clone https://github.com/Petter0x1/egyid.git
|
|
198
|
+
cd egyid
|
|
199
|
+
python -m venv venv
|
|
200
|
+
venv\Scripts\activate # On Windows
|
|
201
|
+
pip install -e .
|
|
202
|
+
python -m unittest discover tests
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
208
|
+
|
|
209
|
+
## Author
|
|
210
|
+
|
|
211
|
+
@Petter0x1
|
egyid-1.0.0/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Egyptian National ID
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/egyid/)
|
|
4
|
+
[](https://pypi.org/project/egyid/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/Petter0x1/egyid/actions)
|
|
7
|
+
|
|
8
|
+
A lightweight and comprehensive Python library for validating, parsing, and generating Egyptian National ID numbers. Built with type hints and designed for reliability in production environments.
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [API Reference](#api-reference)
|
|
16
|
+
- [Testing](#testing)
|
|
17
|
+
- [Contributing](#contributing)
|
|
18
|
+
- [License](#license)
|
|
19
|
+
- [Author](#author)
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Validation**: Comprehensive ID validation including checksum verification, date validation, and governorate checks
|
|
24
|
+
- **Parsing**: Extract birth date, gender, age, governorate, and region information from ID numbers
|
|
25
|
+
- **Generation**: Create valid mock Egyptian National IDs for testing and development
|
|
26
|
+
- **Type Safety**: Full type hints for better IDE support and code reliability
|
|
27
|
+
- **Arabic Support**: Handles Arabic numerals in ID input
|
|
28
|
+
- **Cross-Platform**: Compatible with Python 3.8+
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### From PyPI (Recommended)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install egyid
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### From Source
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/Petter0x1/egyid.git
|
|
42
|
+
cd egyid
|
|
43
|
+
pip install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Requirements
|
|
47
|
+
|
|
48
|
+
- Python 3.8 or higher
|
|
49
|
+
- No external dependencies
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from egyid import EgyptianNationalId
|
|
55
|
+
|
|
56
|
+
# Validate an ID
|
|
57
|
+
is_valid = EgyptianNationalId.is_valid_id("29010280165195")
|
|
58
|
+
print(is_valid) # True
|
|
59
|
+
|
|
60
|
+
# Parse an ID
|
|
61
|
+
eid = EgyptianNationalId("29010280165195")
|
|
62
|
+
if eid.is_valid():
|
|
63
|
+
print(f"Birth Date: {eid.birth_date}") # 1990-10-28
|
|
64
|
+
print(f"Gender: {eid.gender}") # male
|
|
65
|
+
print(f"Governorate: {eid.governorate}") # Cairo
|
|
66
|
+
print(f"Age: {eid.age}") # 35 (as of 2026)
|
|
67
|
+
print(f"Region: {eid.region_name}") # Cairo
|
|
68
|
+
|
|
69
|
+
# Generate a new ID
|
|
70
|
+
new_id = EgyptianNationalId.generate({
|
|
71
|
+
"year": 1995,
|
|
72
|
+
"gender": "female",
|
|
73
|
+
"governorate": "01" # Cairo
|
|
74
|
+
})
|
|
75
|
+
print(new_id) # e.g., "39501010100000" (with correct checksum)
|
|
76
|
+
|
|
77
|
+
# Get full information as dictionary
|
|
78
|
+
info = eid.to_dict()
|
|
79
|
+
print(info)
|
|
80
|
+
# {
|
|
81
|
+
# "nationalId": "29010280165195",
|
|
82
|
+
# "birthYear": 1990,
|
|
83
|
+
# "birthMonth": 10,
|
|
84
|
+
# "birthDay": 28,
|
|
85
|
+
# "age": 35,
|
|
86
|
+
# "gender": "male",
|
|
87
|
+
# "governorate": "Cairo",
|
|
88
|
+
# "region": "Cairo",
|
|
89
|
+
# "insideEgypt": true,
|
|
90
|
+
# "isAdult": true
|
|
91
|
+
# }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### Class: `EgyptianNationalId`
|
|
97
|
+
|
|
98
|
+
#### Methods
|
|
99
|
+
|
|
100
|
+
- `EgyptianNationalId(id_value: Union[str, int])` - Create an instance from an ID string or integer
|
|
101
|
+
- `is_valid() -> bool` - Check if the ID is valid
|
|
102
|
+
- `to_dict() -> Optional[Dict[str, Any]]` - Return ID information as a dictionary (None if invalid)
|
|
103
|
+
|
|
104
|
+
#### Properties
|
|
105
|
+
|
|
106
|
+
- `birth_date: Optional[date]` - Birth date
|
|
107
|
+
- `age: Optional[int]` - Current age
|
|
108
|
+
- `gender: str` - 'male' or 'female'
|
|
109
|
+
- `governorate: Optional[str]` - Governorate name
|
|
110
|
+
- `region_name: str` - Region name
|
|
111
|
+
|
|
112
|
+
#### Class Methods
|
|
113
|
+
|
|
114
|
+
- `is_valid_id(id_value: Union[str, int]) -> bool` - Validate without creating instance
|
|
115
|
+
- `generate(options: Optional[Dict[str, Any]] = None) -> str` - Generate a new valid ID
|
|
116
|
+
- `parse(id_value: Union[str, int]) -> EgyptianNationalId` - Alias for constructor
|
|
117
|
+
- `check_is_male(id_value: Union[str, int]) -> bool` - Check if ID belongs to male
|
|
118
|
+
- `check_is_female(id_value: Union[str, int]) -> bool` - Check if ID belongs to female
|
|
119
|
+
- `check_is_adult(id_value: Union[str, int]) -> bool` - Check if person is 18+
|
|
120
|
+
|
|
121
|
+
### Generation Options
|
|
122
|
+
|
|
123
|
+
The `generate()` method accepts an optional dictionary with:
|
|
124
|
+
- `year: int` - Birth year (default: random 1950-current)
|
|
125
|
+
- `month: int` - Birth month (default: random 1-12)
|
|
126
|
+
- `day: int` - Birth day (default: valid for month/year)
|
|
127
|
+
- `gender: str` - 'male' or 'female' (default: random)
|
|
128
|
+
- `governorate: str` - 2-digit governorate code (default: random)
|
|
129
|
+
|
|
130
|
+
## Testing
|
|
131
|
+
|
|
132
|
+
Run the test suite:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python -m unittest discover tests
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Tests cover validation, parsing, generation, and edge cases across all supported Python versions.
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
Contributions are welcome! Please:
|
|
143
|
+
|
|
144
|
+
1. Fork the repository
|
|
145
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
146
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
147
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
148
|
+
5. Open a Pull Request
|
|
149
|
+
|
|
150
|
+
### Development Setup
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git clone https://github.com/Petter0x1/egyid.git
|
|
154
|
+
cd egyid
|
|
155
|
+
python -m venv venv
|
|
156
|
+
venv\Scripts\activate # On Windows
|
|
157
|
+
pip install -e .
|
|
158
|
+
python -m unittest discover tests
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
164
|
+
|
|
165
|
+
## Author
|
|
166
|
+
|
|
167
|
+
@Petter0x1
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import calendar
|
|
4
|
+
import random
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from .governorates import GOVERNORATES
|
|
9
|
+
from .validation import compute_check_digit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_id(options: Optional[Dict[str, Any]] = None) -> str:
|
|
13
|
+
opts = options or {}
|
|
14
|
+
|
|
15
|
+
year = opts.get("year", random.randint(1950, datetime.now().year))
|
|
16
|
+
month = opts.get("month", random.randint(1, 12))
|
|
17
|
+
max_day = calendar.monthrange(year, month)[1]
|
|
18
|
+
day = opts.get("day", random.randint(1, max_day))
|
|
19
|
+
|
|
20
|
+
if "governorate" in opts:
|
|
21
|
+
gov = str(opts["governorate"]).zfill(2)
|
|
22
|
+
else:
|
|
23
|
+
gov = random.choice(list(GOVERNORATES.keys()))
|
|
24
|
+
|
|
25
|
+
century_digit = 3 if year >= 2000 else 2
|
|
26
|
+
year_digits = str(year)[-2:]
|
|
27
|
+
month_digits = str(month).zfill(2)
|
|
28
|
+
day_digits = str(day).zfill(2)
|
|
29
|
+
|
|
30
|
+
if "gender" in opts:
|
|
31
|
+
is_female = opts["gender"] == "female"
|
|
32
|
+
else:
|
|
33
|
+
is_female = bool(random.getrandbits(1))
|
|
34
|
+
|
|
35
|
+
sequence_start = str(random.randint(0, 999)).zfill(3)
|
|
36
|
+
gender_digit = random.randint(0, 4) * 2 if is_female else random.randint(0, 4) * 2 + 1
|
|
37
|
+
|
|
38
|
+
id_without_check = (
|
|
39
|
+
f"{century_digit}{year_digits}{month_digits}{day_digits}{gov}{sequence_start}{gender_digit}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
check_digit = compute_check_digit(id_without_check)
|
|
43
|
+
return id_without_check + str(check_digit)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ["generate_id"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GovernorateInfo(TypedDict):
|
|
7
|
+
name: str
|
|
8
|
+
region: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
GOVERNORATES: Dict[str, GovernorateInfo] = {
|
|
12
|
+
"01": {"name": "Cairo", "region": "Cairo"},
|
|
13
|
+
"02": {"name": "Alexandria", "region": "Alexandria"},
|
|
14
|
+
"03": {"name": "Port Said", "region": "Canal"},
|
|
15
|
+
"04": {"name": "Suez", "region": "Canal"},
|
|
16
|
+
"11": {"name": "Damietta", "region": "Delta"},
|
|
17
|
+
"12": {"name": "Dakahlia", "region": "Delta"},
|
|
18
|
+
"13": {"name": "Ash Sharqia", "region": "Delta"},
|
|
19
|
+
"14": {"name": "Kalyubia", "region": "Delta"},
|
|
20
|
+
"15": {"name": "Kafr El Sheikh", "region": "Delta"},
|
|
21
|
+
"16": {"name": "Gharbia", "region": "Delta"},
|
|
22
|
+
"17": {"name": "Monufia", "region": "Delta"},
|
|
23
|
+
"18": {"name": "Beheira", "region": "Delta"},
|
|
24
|
+
"19": {"name": "Ismailia", "region": "Canal"},
|
|
25
|
+
"21": {"name": "Giza", "region": "Upper Egypt"},
|
|
26
|
+
"22": {"name": "Beni Suef", "region": "Upper Egypt"},
|
|
27
|
+
"23": {"name": "Fayoum", "region": "Upper Egypt"},
|
|
28
|
+
"24": {"name": "Minya", "region": "Upper Egypt"},
|
|
29
|
+
"25": {"name": "Assiut", "region": "Upper Egypt"},
|
|
30
|
+
"26": {"name": "Sohag", "region": "Upper Egypt"},
|
|
31
|
+
"27": {"name": "Qena", "region": "Upper Egypt"},
|
|
32
|
+
"28": {"name": "Aswan", "region": "Upper Egypt"},
|
|
33
|
+
"29": {"name": "Luxor", "region": "Upper Egypt"},
|
|
34
|
+
"31": {"name": "Red Sea", "region": "Frontier"},
|
|
35
|
+
"32": {"name": "New Valley", "region": "Frontier"},
|
|
36
|
+
"33": {"name": "Matrouh", "region": "Frontier"},
|
|
37
|
+
"34": {"name": "North Sinai", "region": "Frontier"},
|
|
38
|
+
"35": {"name": "South Sinai", "region": "Frontier"},
|
|
39
|
+
"88": {"name": "Outside the Republic", "region": "Foreign"},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_governorate_info(code: str) -> Optional[GovernorateInfo]:
|
|
44
|
+
return GOVERNORATES.get(code)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_governorate_name(code: str) -> Optional[str]:
|
|
48
|
+
info = get_governorate_info(code)
|
|
49
|
+
return info["name"] if info else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_region(code: str) -> str:
|
|
53
|
+
info = get_governorate_info(code)
|
|
54
|
+
return info.get("region", "Foreign") if info else "Foreign"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["GovernorateInfo", "GOVERNORATES", "get_governorate_info", "get_governorate_name", "get_region"]
|
egyid-1.0.0/egyid/id.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
4
|
+
from .generator import generate_id
|
|
5
|
+
from .governorates import get_governorate_name, get_region
|
|
6
|
+
from .utils import sanitize_id
|
|
7
|
+
from .validation import is_valid_id_str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EgyptianNationalId:
|
|
11
|
+
def __init__(self, id_value: Union[str, int]) -> None:
|
|
12
|
+
"""Create a new instance from a raw ID value.
|
|
13
|
+
|
|
14
|
+
The value is converted to string and sanitized immediately.
|
|
15
|
+
"""
|
|
16
|
+
self.id_str: str = sanitize_id(f"{id_value}")
|
|
17
|
+
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
return f"<EgyptianNationalId {self.id_str}>"
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def sanitize(id_value: str) -> str:
|
|
23
|
+
return sanitize_id(id_value)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def parse(cls, id_value: Union[str, int]) -> "EgyptianNationalId":
|
|
27
|
+
return cls(id_value)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def is_valid_id(cls, id_value: Union[str, int]) -> bool:
|
|
31
|
+
try:
|
|
32
|
+
return is_valid_id_str(sanitize_id(f"{id_value}"))
|
|
33
|
+
except Exception:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def check_is_male(cls, id_value: Union[str, int]) -> bool:
|
|
38
|
+
inst = cls(id_value)
|
|
39
|
+
return inst.is_valid() and inst.is_male()
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def check_is_female(cls, id_value: Union[str, int]) -> bool:
|
|
43
|
+
inst = cls(id_value)
|
|
44
|
+
return inst.is_valid() and inst.is_female()
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def check_is_adult(cls, id_value: Union[str, int]) -> bool:
|
|
48
|
+
inst = cls(id_value)
|
|
49
|
+
return inst.is_valid() and inst.is_adult()
|
|
50
|
+
|
|
51
|
+
# --- Generator ---
|
|
52
|
+
@classmethod
|
|
53
|
+
def generate(cls, options: Optional[Dict[str, Any]] = None) -> str:
|
|
54
|
+
return generate_id(options)
|
|
55
|
+
|
|
56
|
+
def is_valid(self) -> bool:
|
|
57
|
+
"""Return True if the ID conforms to all validation rules."""
|
|
58
|
+
return is_valid_id_str(self.id_str)
|
|
59
|
+
|
|
60
|
+
def get_birth_year(self) -> int:
|
|
61
|
+
century_digit = int(self.id_str[0])
|
|
62
|
+
year = int(self.id_str[1:3])
|
|
63
|
+
century = 1900 if century_digit == 2 else 2000
|
|
64
|
+
return century + year
|
|
65
|
+
|
|
66
|
+
def get_birth_month(self) -> int:
|
|
67
|
+
return int(self.id_str[3:5])
|
|
68
|
+
|
|
69
|
+
def get_birth_day(self) -> int:
|
|
70
|
+
return int(self.id_str[5:7])
|
|
71
|
+
|
|
72
|
+
def get_birth_date(self) -> Optional[date]:
|
|
73
|
+
try:
|
|
74
|
+
return date(
|
|
75
|
+
self.get_birth_year(),
|
|
76
|
+
self.get_birth_month(),
|
|
77
|
+
self.get_birth_day(),
|
|
78
|
+
)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def get_age(self) -> Optional[int]:
|
|
83
|
+
bd = self.get_birth_date()
|
|
84
|
+
if bd is None:
|
|
85
|
+
return None
|
|
86
|
+
today = date.today()
|
|
87
|
+
age = today.year - bd.year - ((today.month, today.day) < (bd.month, bd.day))
|
|
88
|
+
return age
|
|
89
|
+
|
|
90
|
+
# convenience property aliases
|
|
91
|
+
@property
|
|
92
|
+
def birth_date(self) -> Optional[date]:
|
|
93
|
+
return self.get_birth_date()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def age(self) -> Optional[int]:
|
|
97
|
+
return self.get_age()
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def gender(self) -> str:
|
|
101
|
+
return self.get_gender()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def governorate(self) -> Optional[str]:
|
|
105
|
+
return self.get_governorate_name()
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def region_name(self) -> str:
|
|
109
|
+
return self.get_region()
|
|
110
|
+
|
|
111
|
+
def get_governorate_code(self) -> str:
|
|
112
|
+
return self.id_str[7:9]
|
|
113
|
+
|
|
114
|
+
def get_governorate_name(self) -> Optional[str]:
|
|
115
|
+
return get_governorate_name(self.get_governorate_code())
|
|
116
|
+
|
|
117
|
+
def get_region(self) -> str:
|
|
118
|
+
return get_region(self.get_governorate_code())
|
|
119
|
+
|
|
120
|
+
def get_gender(self) -> str:
|
|
121
|
+
return "female" if int(self.id_str[12]) % 2 == 0 else "male"
|
|
122
|
+
|
|
123
|
+
def is_male(self) -> bool:
|
|
124
|
+
return self.get_gender() == "male"
|
|
125
|
+
|
|
126
|
+
def is_female(self) -> bool:
|
|
127
|
+
return self.get_gender() == "female"
|
|
128
|
+
|
|
129
|
+
def is_adult(self) -> bool:
|
|
130
|
+
age = self.get_age()
|
|
131
|
+
return age is not None and age >= 18
|
|
132
|
+
|
|
133
|
+
def is_inside_egypt(self) -> bool:
|
|
134
|
+
return self.get_governorate_code() != "88"
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> Optional[Dict[str, Any]]:
|
|
137
|
+
if not self.is_valid():
|
|
138
|
+
return None
|
|
139
|
+
return {
|
|
140
|
+
"nationalId": self.id_str,
|
|
141
|
+
"birthYear": self.get_birth_year(),
|
|
142
|
+
"birthMonth": self.get_birth_month(),
|
|
143
|
+
"birthDay": self.get_birth_day(),
|
|
144
|
+
"age": self.get_age(),
|
|
145
|
+
"gender": self.get_gender(),
|
|
146
|
+
"governorate": self.get_governorate_name(),
|
|
147
|
+
"region": self.get_region(),
|
|
148
|
+
"insideEgypt": self.is_inside_egypt(),
|
|
149
|
+
"isAdult": self.is_adult(),
|
|
150
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sanitize_id(id_value: str) -> str:
|
|
7
|
+
"""Return digits-only version of the ID, converting Arabic numerals."""
|
|
8
|
+
trans = str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789")
|
|
9
|
+
cleaned = id_value.translate(trans)
|
|
10
|
+
return re.sub(r"[^\d]", "", cleaned)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ["sanitize_id"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from .governorates import get_governorate_info
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_MULTIPLIERS = (2, 7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_valid_date(year: int, month: int, day: int) -> bool:
|
|
11
|
+
try:
|
|
12
|
+
datetime(year, month, day)
|
|
13
|
+
return True
|
|
14
|
+
except ValueError:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _compute_check_digit(id_without_check: str) -> int:
|
|
19
|
+
total = sum(int(id_without_check[i]) * _MULTIPLIERS[i] for i in range(13))
|
|
20
|
+
remainder = total % 11
|
|
21
|
+
if remainder == 0:
|
|
22
|
+
return 0
|
|
23
|
+
else:
|
|
24
|
+
return (11 - remainder) % 10
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_valid_id_str(id_str: str) -> bool:
|
|
28
|
+
if not re.fullmatch(r"\d{14}", id_str):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
if int(id_str[0]) not in (2, 3):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
year = int(id_str[1:3])
|
|
35
|
+
month = int(id_str[3:5])
|
|
36
|
+
day = int(id_str[5:7])
|
|
37
|
+
century = 1900 if int(id_str[0]) == 2 else 2000
|
|
38
|
+
|
|
39
|
+
if not _is_valid_date(century + year, month, day):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
if century + year > datetime.now().year:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if get_governorate_info(id_str[7:9]) is None:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
return (_compute_check_digit(id_str[:13]) == int(id_str[13]))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compute_check_digit(id_without_check: str) -> int:
|
|
52
|
+
return _compute_check_digit(id_without_check)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["is_valid_id_str", "compute_check_digit"]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: egyid
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python library for parsing, validating, and generating Egyptian National IDs
|
|
5
|
+
Author: Petter0x1
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Petter0x1
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/Petter0x1/egyid
|
|
28
|
+
Project-URL: Repository, https://github.com/Petter0x1/egyid
|
|
29
|
+
Project-URL: Issues, https://github.com/Petter0x1/egyid/issues
|
|
30
|
+
Keywords: egypt,national-id,validation,parsing,generation
|
|
31
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# Egyptian National ID
|
|
46
|
+
|
|
47
|
+
[](https://pypi.org/project/egyid/)
|
|
48
|
+
[](https://pypi.org/project/egyid/)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
[](https://github.com/Petter0x1/egyid/actions)
|
|
51
|
+
|
|
52
|
+
A lightweight and comprehensive Python library for validating, parsing, and generating Egyptian National ID numbers. Built with type hints and designed for reliability in production environments.
|
|
53
|
+
|
|
54
|
+
## Table of Contents
|
|
55
|
+
|
|
56
|
+
- [Features](#features)
|
|
57
|
+
- [Installation](#installation)
|
|
58
|
+
- [Quick Start](#quick-start)
|
|
59
|
+
- [API Reference](#api-reference)
|
|
60
|
+
- [Testing](#testing)
|
|
61
|
+
- [Contributing](#contributing)
|
|
62
|
+
- [License](#license)
|
|
63
|
+
- [Author](#author)
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
- **Validation**: Comprehensive ID validation including checksum verification, date validation, and governorate checks
|
|
68
|
+
- **Parsing**: Extract birth date, gender, age, governorate, and region information from ID numbers
|
|
69
|
+
- **Generation**: Create valid mock Egyptian National IDs for testing and development
|
|
70
|
+
- **Type Safety**: Full type hints for better IDE support and code reliability
|
|
71
|
+
- **Arabic Support**: Handles Arabic numerals in ID input
|
|
72
|
+
- **Cross-Platform**: Compatible with Python 3.8+
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
### From PyPI (Recommended)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install egyid
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### From Source
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/Petter0x1/egyid.git
|
|
86
|
+
cd egyid
|
|
87
|
+
pip install .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Requirements
|
|
91
|
+
|
|
92
|
+
- Python 3.8 or higher
|
|
93
|
+
- No external dependencies
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from egyid import EgyptianNationalId
|
|
99
|
+
|
|
100
|
+
# Validate an ID
|
|
101
|
+
is_valid = EgyptianNationalId.is_valid_id("29010280165195")
|
|
102
|
+
print(is_valid) # True
|
|
103
|
+
|
|
104
|
+
# Parse an ID
|
|
105
|
+
eid = EgyptianNationalId("29010280165195")
|
|
106
|
+
if eid.is_valid():
|
|
107
|
+
print(f"Birth Date: {eid.birth_date}") # 1990-10-28
|
|
108
|
+
print(f"Gender: {eid.gender}") # male
|
|
109
|
+
print(f"Governorate: {eid.governorate}") # Cairo
|
|
110
|
+
print(f"Age: {eid.age}") # 35 (as of 2026)
|
|
111
|
+
print(f"Region: {eid.region_name}") # Cairo
|
|
112
|
+
|
|
113
|
+
# Generate a new ID
|
|
114
|
+
new_id = EgyptianNationalId.generate({
|
|
115
|
+
"year": 1995,
|
|
116
|
+
"gender": "female",
|
|
117
|
+
"governorate": "01" # Cairo
|
|
118
|
+
})
|
|
119
|
+
print(new_id) # e.g., "39501010100000" (with correct checksum)
|
|
120
|
+
|
|
121
|
+
# Get full information as dictionary
|
|
122
|
+
info = eid.to_dict()
|
|
123
|
+
print(info)
|
|
124
|
+
# {
|
|
125
|
+
# "nationalId": "29010280165195",
|
|
126
|
+
# "birthYear": 1990,
|
|
127
|
+
# "birthMonth": 10,
|
|
128
|
+
# "birthDay": 28,
|
|
129
|
+
# "age": 35,
|
|
130
|
+
# "gender": "male",
|
|
131
|
+
# "governorate": "Cairo",
|
|
132
|
+
# "region": "Cairo",
|
|
133
|
+
# "insideEgypt": true,
|
|
134
|
+
# "isAdult": true
|
|
135
|
+
# }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API Reference
|
|
139
|
+
|
|
140
|
+
### Class: `EgyptianNationalId`
|
|
141
|
+
|
|
142
|
+
#### Methods
|
|
143
|
+
|
|
144
|
+
- `EgyptianNationalId(id_value: Union[str, int])` - Create an instance from an ID string or integer
|
|
145
|
+
- `is_valid() -> bool` - Check if the ID is valid
|
|
146
|
+
- `to_dict() -> Optional[Dict[str, Any]]` - Return ID information as a dictionary (None if invalid)
|
|
147
|
+
|
|
148
|
+
#### Properties
|
|
149
|
+
|
|
150
|
+
- `birth_date: Optional[date]` - Birth date
|
|
151
|
+
- `age: Optional[int]` - Current age
|
|
152
|
+
- `gender: str` - 'male' or 'female'
|
|
153
|
+
- `governorate: Optional[str]` - Governorate name
|
|
154
|
+
- `region_name: str` - Region name
|
|
155
|
+
|
|
156
|
+
#### Class Methods
|
|
157
|
+
|
|
158
|
+
- `is_valid_id(id_value: Union[str, int]) -> bool` - Validate without creating instance
|
|
159
|
+
- `generate(options: Optional[Dict[str, Any]] = None) -> str` - Generate a new valid ID
|
|
160
|
+
- `parse(id_value: Union[str, int]) -> EgyptianNationalId` - Alias for constructor
|
|
161
|
+
- `check_is_male(id_value: Union[str, int]) -> bool` - Check if ID belongs to male
|
|
162
|
+
- `check_is_female(id_value: Union[str, int]) -> bool` - Check if ID belongs to female
|
|
163
|
+
- `check_is_adult(id_value: Union[str, int]) -> bool` - Check if person is 18+
|
|
164
|
+
|
|
165
|
+
### Generation Options
|
|
166
|
+
|
|
167
|
+
The `generate()` method accepts an optional dictionary with:
|
|
168
|
+
- `year: int` - Birth year (default: random 1950-current)
|
|
169
|
+
- `month: int` - Birth month (default: random 1-12)
|
|
170
|
+
- `day: int` - Birth day (default: valid for month/year)
|
|
171
|
+
- `gender: str` - 'male' or 'female' (default: random)
|
|
172
|
+
- `governorate: str` - 2-digit governorate code (default: random)
|
|
173
|
+
|
|
174
|
+
## Testing
|
|
175
|
+
|
|
176
|
+
Run the test suite:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python -m unittest discover tests
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Tests cover validation, parsing, generation, and edge cases across all supported Python versions.
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
Contributions are welcome! Please:
|
|
187
|
+
|
|
188
|
+
1. Fork the repository
|
|
189
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
190
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
191
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
192
|
+
5. Open a Pull Request
|
|
193
|
+
|
|
194
|
+
### Development Setup
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git clone https://github.com/Petter0x1/egyid.git
|
|
198
|
+
cd egyid
|
|
199
|
+
python -m venv venv
|
|
200
|
+
venv\Scripts\activate # On Windows
|
|
201
|
+
pip install -e .
|
|
202
|
+
python -m unittest discover tests
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
208
|
+
|
|
209
|
+
## Author
|
|
210
|
+
|
|
211
|
+
@Petter0x1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
egyid/__init__.py
|
|
5
|
+
egyid/generator.py
|
|
6
|
+
egyid/governorates.py
|
|
7
|
+
egyid/id.py
|
|
8
|
+
egyid/utils.py
|
|
9
|
+
egyid/validation.py
|
|
10
|
+
egyid.egg-info/PKG-INFO
|
|
11
|
+
egyid.egg-info/SOURCES.txt
|
|
12
|
+
egyid.egg-info/dependency_links.txt
|
|
13
|
+
egyid.egg-info/top_level.txt
|
|
14
|
+
tests/test_egyptian_national_id.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
egyid
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "egyid"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A Python library for parsing, validating, and generating Egyptian National IDs"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Petter0x1" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = { file = "LICENSE" }
|
|
15
|
+
|
|
16
|
+
keywords = ["egypt", "national-id", "validation", "parsing", "generation"]
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/Petter0x1/egyid"
|
|
34
|
+
Repository = "https://github.com/Petter0x1/egyid"
|
|
35
|
+
Issues = "https://github.com/Petter0x1/egyid/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["egyid*"]
|
egyid-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import date
|
|
3
|
+
from egyid import EgyptianNationalId
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestEgyptianNationalId(unittest.TestCase):
|
|
7
|
+
def test_valid_id(self):
|
|
8
|
+
# The last digit is a check digit, calculated using the standard Egyptian national ID algorithm.
|
|
9
|
+
self.assertTrue(EgyptianNationalId.is_valid_id("29001011400014"))
|
|
10
|
+
|
|
11
|
+
def test_invalid_id(self):
|
|
12
|
+
self.assertFalse(EgyptianNationalId.is_valid_id("00000000000000"))
|
|
13
|
+
|
|
14
|
+
def test_generate_valid(self):
|
|
15
|
+
generated = EgyptianNationalId.generate({"year": 1990, "gender": "male", "governorate": "01"})
|
|
16
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(generated))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestEgyptianNationalIdComprehensive(unittest.TestCase):
|
|
20
|
+
"""Comprehensive test suite for Egyptian National ID validation and parsing."""
|
|
21
|
+
|
|
22
|
+
def test_valid_ids(self):
|
|
23
|
+
"""Test various valid Egyptian National IDs."""
|
|
24
|
+
valid_ids = [
|
|
25
|
+
"29001011400014", # 1990-01-01, Kalyubia, male
|
|
26
|
+
"29010280165195", # 1990-10-28, Cairo, male
|
|
27
|
+
"30105201501238", # 2001-05-20, Cairo, female
|
|
28
|
+
"28801010100001", # 1988-01-01, Cairo, male (adjusted checksum)
|
|
29
|
+
"29512310123455", # 1995-12-31, Cairo, female
|
|
30
|
+
]
|
|
31
|
+
for id_str in valid_ids:
|
|
32
|
+
with self.subTest(id=id_str):
|
|
33
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(id_str))
|
|
34
|
+
eid = EgyptianNationalId(id_str)
|
|
35
|
+
self.assertTrue(eid.is_valid())
|
|
36
|
+
self.assertIsNotNone(eid.birth_date)
|
|
37
|
+
self.assertIn(eid.gender, ['male', 'female'])
|
|
38
|
+
self.assertIsNotNone(eid.governorate)
|
|
39
|
+
|
|
40
|
+
def test_invalid_lengths(self):
|
|
41
|
+
"""Test IDs with wrong lengths."""
|
|
42
|
+
invalid_lengths = [
|
|
43
|
+
"123", # Too short
|
|
44
|
+
"2900101140001", # 13 digits
|
|
45
|
+
"290010114000145", # 15 digits
|
|
46
|
+
"", # Empty
|
|
47
|
+
]
|
|
48
|
+
for id_str in invalid_lengths:
|
|
49
|
+
with self.subTest(id=id_str):
|
|
50
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
51
|
+
|
|
52
|
+
def test_invalid_century_digits(self):
|
|
53
|
+
"""Test IDs with invalid century digits."""
|
|
54
|
+
invalid_centuries = [
|
|
55
|
+
"19001011400014", # Starts with 1
|
|
56
|
+
"49001011400014", # Starts with 4
|
|
57
|
+
"09001011400014", # Starts with 0
|
|
58
|
+
]
|
|
59
|
+
for id_str in invalid_centuries:
|
|
60
|
+
with self.subTest(id=id_str):
|
|
61
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
62
|
+
|
|
63
|
+
def test_invalid_dates(self):
|
|
64
|
+
"""Test IDs with invalid birth dates."""
|
|
65
|
+
invalid_dates = [
|
|
66
|
+
"29023011400014", # Feb 30 (invalid)
|
|
67
|
+
"29023111400014", # Feb 31 (invalid)
|
|
68
|
+
"29013211400014", # Jan 32 (invalid)
|
|
69
|
+
"29001311400014", # Month 13 (invalid)
|
|
70
|
+
"29010011400014", # Month 00 (invalid)
|
|
71
|
+
"29000111400014", # Day 00 (invalid)
|
|
72
|
+
]
|
|
73
|
+
for id_str in invalid_dates:
|
|
74
|
+
with self.subTest(id=id_str):
|
|
75
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
76
|
+
|
|
77
|
+
def test_future_dates(self):
|
|
78
|
+
"""Test IDs with future birth dates."""
|
|
79
|
+
# Assuming current year is 2026, future dates should be invalid
|
|
80
|
+
future_ids = [
|
|
81
|
+
"33601011400018", # 2033-01-01 (future)
|
|
82
|
+
]
|
|
83
|
+
for id_str in future_ids:
|
|
84
|
+
with self.subTest(id=id_str):
|
|
85
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
86
|
+
|
|
87
|
+
def test_invalid_governorates(self):
|
|
88
|
+
"""Test IDs with invalid governorate codes."""
|
|
89
|
+
invalid_govs = [
|
|
90
|
+
"29001000400014", # Gov 00 (invalid)
|
|
91
|
+
"29001991400014", # Gov 99 (invalid)
|
|
92
|
+
"29001041400014", # Gov 41 (invalid)
|
|
93
|
+
]
|
|
94
|
+
for id_str in invalid_govs:
|
|
95
|
+
with self.subTest(id=id_str):
|
|
96
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
97
|
+
|
|
98
|
+
def test_invalid_checksums(self):
|
|
99
|
+
"""Test IDs with wrong check digits."""
|
|
100
|
+
invalid_checksums = [
|
|
101
|
+
"29001011400015", # Wrong checksum
|
|
102
|
+
"29001011400013", # Wrong checksum
|
|
103
|
+
"29001011400010", # Wrong checksum
|
|
104
|
+
]
|
|
105
|
+
for id_str in invalid_checksums:
|
|
106
|
+
with self.subTest(id=id_str):
|
|
107
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
108
|
+
|
|
109
|
+
def test_non_numeric_ids(self):
|
|
110
|
+
"""Test IDs with non-numeric characters."""
|
|
111
|
+
non_numeric = [
|
|
112
|
+
"2900101140001a", # Letter
|
|
113
|
+
"2900101140001 ", # Space
|
|
114
|
+
"2900101140001-", # Dash
|
|
115
|
+
]
|
|
116
|
+
for id_str in non_numeric:
|
|
117
|
+
with self.subTest(id=id_str):
|
|
118
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(id_str))
|
|
119
|
+
|
|
120
|
+
def test_arabic_numerals(self):
|
|
121
|
+
"""Test IDs with Arabic numerals (should be sanitized)."""
|
|
122
|
+
arabic_id = "٢٩٠١٠٢٨٠١٦٥١٩٥" # 29010280165195 in Arabic
|
|
123
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(arabic_id))
|
|
124
|
+
|
|
125
|
+
def test_parsing_methods(self):
|
|
126
|
+
"""Test parsing methods for valid ID."""
|
|
127
|
+
eid = EgyptianNationalId("29010280165195")
|
|
128
|
+
self.assertEqual(eid.get_birth_year(), 1990)
|
|
129
|
+
self.assertEqual(eid.get_birth_month(), 10)
|
|
130
|
+
self.assertEqual(eid.get_birth_day(), 28)
|
|
131
|
+
self.assertEqual(eid.birth_date, date(1990, 10, 28))
|
|
132
|
+
self.assertEqual(eid.gender, "male")
|
|
133
|
+
self.assertEqual(eid.governorate, "Cairo")
|
|
134
|
+
self.assertEqual(eid.region_name, "Cairo")
|
|
135
|
+
self.assertTrue(eid.is_male())
|
|
136
|
+
self.assertFalse(eid.is_female())
|
|
137
|
+
self.assertTrue(eid.is_adult())
|
|
138
|
+
self.assertTrue(eid.is_inside_egypt())
|
|
139
|
+
|
|
140
|
+
def test_generation(self):
|
|
141
|
+
"""Test ID generation."""
|
|
142
|
+
generated = EgyptianNationalId.generate()
|
|
143
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(generated))
|
|
144
|
+
self.assertEqual(len(generated), 14)
|
|
145
|
+
|
|
146
|
+
# Test with options
|
|
147
|
+
male_id = EgyptianNationalId.generate({"gender": "male", "year": 1990, "governorate": "01"})
|
|
148
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(male_id))
|
|
149
|
+
eid = EgyptianNationalId(male_id)
|
|
150
|
+
self.assertEqual(eid.get_birth_year(), 1990)
|
|
151
|
+
self.assertEqual(eid.gender, "male")
|
|
152
|
+
|
|
153
|
+
def test_to_dict(self):
|
|
154
|
+
"""Test dictionary output."""
|
|
155
|
+
eid = EgyptianNationalId("29010280165195")
|
|
156
|
+
data = eid.to_dict()
|
|
157
|
+
expected = {
|
|
158
|
+
"nationalId": "29010280165195",
|
|
159
|
+
"birthYear": 1990,
|
|
160
|
+
"birthMonth": 10,
|
|
161
|
+
"birthDay": 28,
|
|
162
|
+
"age": 35, # As of 2025
|
|
163
|
+
"gender": "male",
|
|
164
|
+
"governorate": "Cairo",
|
|
165
|
+
"region": "Cairo",
|
|
166
|
+
"insideEgypt": True,
|
|
167
|
+
"isAdult": True,
|
|
168
|
+
}
|
|
169
|
+
self.assertEqual(data, expected)
|
|
170
|
+
|
|
171
|
+
def test_invalid_to_dict(self):
|
|
172
|
+
"""Test to_dict returns None for invalid IDs."""
|
|
173
|
+
eid = EgyptianNationalId("00000000000000")
|
|
174
|
+
self.assertIsNone(eid.to_dict())
|
|
175
|
+
|
|
176
|
+
def test_class_methods(self):
|
|
177
|
+
"""Test class methods for quick checks."""
|
|
178
|
+
self.assertTrue(EgyptianNationalId.check_is_male("29010280165195"))
|
|
179
|
+
self.assertFalse(EgyptianNationalId.check_is_female("29010280165195"))
|
|
180
|
+
self.assertTrue(EgyptianNationalId.check_is_adult("29010280165195"))
|
|
181
|
+
|
|
182
|
+
self.assertFalse(EgyptianNationalId.check_is_male("30105201501220")) # Female
|
|
183
|
+
self.assertTrue(EgyptianNationalId.check_is_female("30105201501220"))
|
|
184
|
+
|
|
185
|
+
def test_sanitization(self):
|
|
186
|
+
"""Test ID sanitization."""
|
|
187
|
+
self.assertEqual(EgyptianNationalId.sanitize("29001011400014"), "29001011400014")
|
|
188
|
+
self.assertEqual(EgyptianNationalId.sanitize("٢٩٠٠١٠١١٤٠٠٠١٤"), "29001011400014")
|
|
189
|
+
self.assertEqual(EgyptianNationalId.sanitize("290-010-114-000-14"), "29001011400014")
|
|
190
|
+
|
|
191
|
+
def test_edge_cases(self):
|
|
192
|
+
"""Test various edge cases."""
|
|
193
|
+
# Leap year
|
|
194
|
+
leap_id = "29602291400018" # 1996-02-29
|
|
195
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(leap_id))
|
|
196
|
+
|
|
197
|
+
# Non-leap year Feb 29
|
|
198
|
+
invalid_leap = "29702291400014" # 1997-02-29 (invalid)
|
|
199
|
+
self.assertFalse(EgyptianNationalId.is_valid_id(invalid_leap))
|
|
200
|
+
|
|
201
|
+
# Century boundary
|
|
202
|
+
century_1900 = "29912311400019" # 1999-12-31
|
|
203
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(century_1900))
|
|
204
|
+
|
|
205
|
+
century_2000 = "30101011400014" # 2001-01-01
|
|
206
|
+
self.assertTrue(EgyptianNationalId.is_valid_id(century_2000))
|
|
207
|
+
|
|
208
|
+
def test_foreign_governorate(self):
|
|
209
|
+
"""Test IDs from outside Egypt."""
|
|
210
|
+
foreign_id = "29001018800018" # Gov 88 (Outside Republic)
|
|
211
|
+
eid = EgyptianNationalId(foreign_id)
|
|
212
|
+
self.assertEqual(eid.governorate, "Outside the Republic")
|
|
213
|
+
self.assertEqual(eid.region_name, "Foreign")
|
|
214
|
+
self.assertFalse(eid.is_inside_egypt())
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
unittest.main()
|