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 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
+ [![PyPI version](https://img.shields.io/pypi/v/egyid.svg)](https://pypi.org/project/egyid/)
48
+ [![Python versions](https://img.shields.io/pypi/pyversions/egyid.svg)](https://pypi.org/project/egyid/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
50
+ [![CI](https://github.com/Petter0x1/egyid/actions/workflows/ci.yml/badge.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/egyid.svg)](https://pypi.org/project/egyid/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/egyid.svg)](https://pypi.org/project/egyid/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![CI](https://github.com/Petter0x1/egyid/actions/workflows/ci.yml/badge.svg)](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,3 @@
1
+ from .id import EgyptianNationalId
2
+
3
+ __all__ = ["EgyptianNationalId"]
@@ -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"]
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/egyid.svg)](https://pypi.org/project/egyid/)
48
+ [![Python versions](https://img.shields.io/pypi/pyversions/egyid.svg)](https://pypi.org/project/egyid/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
50
+ [![CI](https://github.com/Petter0x1/egyid/actions/workflows/ci.yml/badge.svg)](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
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()