-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(analyzer): add nine South African predefined recognizers #2069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SharonHart
merged 9 commits into
data-privacy-stack:main
from
thatomokoena:feature/za-recognizers
Jul 23, 2026
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
b4bbd5f
feat(analyzer): add nine South African predefined recognizers
thatomokoena dfe1edd
fix(analyzer): update year validation logic in South African recognizers
thatomokoena 332161a
Merge branch 'main' into feature/za-recognizers
thatomokoena 1603618
fix(analyzer): address Copilot review feedback for ZA recognizers
thatomokoena 1701c15
Merge branch 'main' into feature/za-recognizers
thatomokoena 746f398
fix(analyzer): align ZA driver licence docs and address Copilot round 3
thatomokoena 4e63bba
Merge branch 'main' into feature/za-recognizers
SharonHart 772b912
Update CHANGELOG.md
SharonHart d2bac32
Update CHANGELOG.md
SharonHart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
...alyzer/presidio_analyzer/predefined_recognizers/country_specific/south_africa/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,27 @@ | ||
| """South Africa-specific recognizers.""" | ||
|
|
||
| from .za_company_registration_recognizer import ZaCompanyRegistrationRecognizer | ||
| from .za_driver_license_recognizer import ZaDriverLicenseRecognizer | ||
| from .za_id_number_recognizer import ZaIdNumberRecognizer | ||
| from .za_income_tax_number_recognizer import ZaIncomeTaxNumberRecognizer | ||
| from .za_license_plate_recognizer import ZaLicensePlateRecognizer | ||
| from .za_passport_recognizer import ZaPassportRecognizer | ||
| from .za_phone_number_recognizer import ( | ||
| ZaMobileNumberRecognizer, | ||
| ZaTelephoneNumberRecognizer, | ||
| ) | ||
| from .za_traffic_register_number_recognizer import ZaTrafficRegisterNumberRecognizer | ||
| from .za_vat_number_recognizer import ZaVatNumberRecognizer | ||
|
|
||
| __all__ = [ | ||
| "ZaCompanyRegistrationRecognizer", | ||
| "ZaDriverLicenseRecognizer", | ||
| "ZaIdNumberRecognizer", | ||
| "ZaIncomeTaxNumberRecognizer", | ||
| "ZaLicensePlateRecognizer", | ||
| "ZaMobileNumberRecognizer", | ||
| "ZaPassportRecognizer", | ||
| "ZaTelephoneNumberRecognizer", | ||
| "ZaTrafficRegisterNumberRecognizer", | ||
| "ZaVatNumberRecognizer", | ||
| ] |
105 changes: 105 additions & 0 deletions
105
...redefined_recognizers/country_specific/south_africa/za_company_registration_recognizer.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| from datetime import date | ||
| from typing import List, Optional | ||
|
|
||
| from presidio_analyzer import Pattern, PatternRecognizer | ||
|
|
||
|
|
||
| class ZaCompanyRegistrationRecognizer(PatternRecognizer): | ||
| """ | ||
| Recognize South African company registration numbers (CIPC). | ||
|
|
||
| Modern private and public companies use ``YYYY/NNNNNN/NN`` (year, | ||
| sequence, company-type suffix). Legacy formats include prefixed codes | ||
| such as ``CK`` (close corporation) and other CIPC entity prefixes. | ||
|
|
||
| Reference: | ||
| https://support.tradeshield.ai/support/solutions/articles/153000256853-cipc-company-codes-types-status-the-complete-guide | ||
|
|
||
| :param patterns: List of patterns to be used by this recognizer | ||
| :param context: List of context words to increase confidence in detection | ||
| :param supported_language: Language this recognizer supports | ||
| :param supported_entity: The entity this recognizer can detect | ||
| """ | ||
|
|
||
| COUNTRY_CODE = "za" | ||
|
|
||
| LEGACY_PREFIXES = frozenset({"CK", "K", "T", "W", "B", "M", "N", "NR"}) | ||
|
|
||
| PATTERNS = [ | ||
| Pattern( | ||
| "South African Company Registration (modern)", | ||
| r"\b(?:19|20)\d{2}/\d{6}/\d{2}\b", | ||
| 0.4, | ||
| ), | ||
| Pattern( | ||
| "South African Company Registration (legacy)", | ||
| r"\b(?:CK|K|T|W|B|M|N|NR)\d{4}/\d{6}\b", | ||
| 0.3, | ||
| ), | ||
| ] | ||
|
|
||
| CONTEXT = [ | ||
| "cipc", | ||
| "company registration", | ||
| "registration number", | ||
| "close corporation", | ||
| "company reg", | ||
| "enterprise number", | ||
| ] | ||
|
|
||
| def __init__( | ||
| self, | ||
| patterns: Optional[List[Pattern]] = None, | ||
| context: Optional[List[str]] = None, | ||
| supported_language: str = "en", | ||
| supported_entity: str = "ZA_COMPANY_REGISTRATION", | ||
| name: Optional[str] = None, | ||
| ): | ||
| patterns = self.PATTERNS if patterns is None else patterns | ||
| context = self.CONTEXT if context is None else context | ||
| super().__init__( | ||
| supported_entity=supported_entity, | ||
| patterns=patterns, | ||
| context=context, | ||
| supported_language=supported_language, | ||
| name=name, | ||
| ) | ||
|
|
||
| def validate_result(self, pattern_text: str) -> bool: # noqa: D102 | ||
| text = pattern_text.upper() | ||
| parts = text.split("/") | ||
| if len(parts) == 3 and parts[0].isdigit(): | ||
| return self._validate_modern_format(text) | ||
| if len(parts) == 2: | ||
| return self._validate_legacy_format(text) | ||
| return False | ||
|
|
||
| def _validate_modern_format(self, text: str) -> bool: | ||
| parts = text.split("/") | ||
| if len(parts) != 3: | ||
| return False | ||
| year_part, sequence_part, type_part = parts | ||
| if not ( | ||
| year_part.isdigit() | ||
| and sequence_part.isdigit() | ||
| and type_part.isdigit() | ||
| ): | ||
| return False | ||
| if len(year_part) != 4 or len(sequence_part) != 6 or len(type_part) != 2: | ||
| return False | ||
| year = int(year_part) | ||
| return 1800 <= year <= date.today().year | ||
|
|
||
| def _validate_legacy_format(self, text: str) -> bool: | ||
| slash_index = text.index("/") | ||
| prefix = text[:slash_index] | ||
| sequence = text[slash_index + 1 :] | ||
| if not sequence.isdigit() or len(sequence) != 6: | ||
| return False | ||
| for legacy_prefix in sorted(self.LEGACY_PREFIXES, key=len, reverse=True): | ||
| if prefix.startswith(legacy_prefix): | ||
| year_part = prefix[len(legacy_prefix) :] | ||
| if len(year_part) == 4 and year_part.isdigit(): | ||
| year = int(year_part) | ||
| return 1800 <= year <= date.today().year | ||
| return False | ||
75 changes: 75 additions & 0 deletions
75
...yzer/predefined_recognizers/country_specific/south_africa/za_driver_license_recognizer.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import re | ||
| from typing import List, Optional | ||
|
|
||
| from presidio_analyzer import Pattern, PatternRecognizer | ||
|
|
||
|
|
||
| class ZaDriverLicenseRecognizer(PatternRecognizer): | ||
| """ | ||
| Recognize South African driver's licence numbers issued by eNaTIS. | ||
|
|
||
| eNaTIS licence numbers are alphanumeric strings of 10–14 | ||
| characters combining digit blocks with trailing letter groups. | ||
|
|
||
| Reference: | ||
| https://github.com/ugommirikwe/sa-license-decoder/blob/master/SPEC.md | ||
|
|
||
| :param patterns: List of patterns to be used by this recognizer | ||
| :param context: List of context words to increase confidence in detection | ||
| :param supported_language: Language this recognizer supports | ||
| :param supported_entity: The entity this recognizer can detect | ||
| """ | ||
|
|
||
| COUNTRY_CODE = "za" | ||
|
|
||
| MIN_LENGTH = 10 | ||
| MAX_LENGTH = 14 | ||
|
|
||
| PATTERNS = [ | ||
| Pattern( | ||
| "South African Driver's Licence", | ||
| r"\b\d{6,10}[A-Z0-9]{2,5}\b", | ||
| 0.3, | ||
| ), | ||
| ] | ||
|
thatomokoena marked this conversation as resolved.
|
||
|
|
||
| CONTEXT = [ | ||
| "licence", | ||
| "license", | ||
| "driving licence", | ||
| "driving license", | ||
| "driver's licence", | ||
| "driver's license", | ||
| "drivers licence", | ||
| "drivers license", | ||
| "enatis", | ||
| "natis", | ||
| "licence number", | ||
| "license number", | ||
| ] | ||
|
|
||
| def __init__( | ||
| self, | ||
| patterns: Optional[List[Pattern]] = None, | ||
| context: Optional[List[str]] = None, | ||
| supported_language: str = "en", | ||
| supported_entity: str = "ZA_DRIVER_LICENSE", | ||
| name: Optional[str] = None, | ||
| ): | ||
| patterns = self.PATTERNS if patterns is None else patterns | ||
| context = self.CONTEXT if context is None else context | ||
| super().__init__( | ||
| supported_entity=supported_entity, | ||
| patterns=patterns, | ||
| context=context, | ||
| supported_language=supported_language, | ||
| name=name, | ||
| ) | ||
|
|
||
| def validate_result(self, pattern_text: str) -> bool: # noqa: D102 | ||
| text = pattern_text.upper() | ||
| if not self.MIN_LENGTH <= len(text) <= self.MAX_LENGTH: | ||
| return False | ||
| if re.fullmatch(r"\d{6,10}[A-Z0-9]{2,5}", text) is None: | ||
| return False | ||
| return bool(re.search(r"[A-Z]", text)) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.