Coverage for presidio_analyzer / predefined_recognizers / country_specific / us / medical_license_recognizer.py: 100%
25 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
1from typing import List, Optional, Tuple
3from presidio_analyzer import EntityRecognizer, Pattern, PatternRecognizer
5# https://www.meditec.com/blog/dea-numbers-what-do-they-mean
8class MedicalLicenseRecognizer(PatternRecognizer):
9 """
10 Recognize common Medical license numbers using regex + checksum.
12 :param patterns: List of patterns to be used by this recognizer
13 :param context: List of context words to increase confidence in detection
14 :param supported_language: Language this recognizer supports
15 :param supported_entity: The entity this recognizer can detect
16 :param replacement_pairs: List of tuples with potential replacement values
17 for different strings to be used during pattern matching.
18 This can allow a greater variety in input, for example by removing dashes or spaces.
19 """
21 PATTERNS = [
22 Pattern(
23 "USA DEA Certificate Number (weak)",
24 r"[abcdefghjklmprstuxABCDEFGHJKLMPRSTUX]{1}[a-zA-Z]{1}\d{7}|"
25 r"[abcdefghjklmprstuxABCDEFGHJKLMPRSTUX]{1}9\d{7}",
26 0.4,
27 ),
28 ]
30 CONTEXT = ["medical", "certificate", "DEA"]
32 def __init__(
33 self,
34 patterns: Optional[List[Pattern]] = None,
35 context: Optional[List[str]] = None,
36 supported_language: str = "en",
37 supported_entity: str = "MEDICAL_LICENSE",
38 replacement_pairs: Optional[List[Tuple[str, str]]] = None,
39 name: Optional[str] = None,
40 ):
41 self.replacement_pairs = (
42 replacement_pairs if replacement_pairs else [("-", ""), (" ", "")]
43 )
44 patterns = patterns if patterns else self.PATTERNS
45 context = context if context else self.CONTEXT
46 super().__init__(
47 supported_entity=supported_entity,
48 patterns=patterns,
49 context=context,
50 supported_language=supported_language,
51 name=name,
52 )
54 def validate_result(self, pattern_text: str) -> bool: # noqa: D102
55 sanitized_value = EntityRecognizer.sanitize_value(
56 pattern_text, self.replacement_pairs
57 )
58 checksum = self.__luhn_checksum(sanitized_value)
60 return checksum
62 @staticmethod
63 def __luhn_checksum(sanitized_value: str) -> bool:
64 def digits_of(n: str) -> List[int]:
65 return [int(dig) for dig in str(n)]
67 digits = digits_of(sanitized_value[2:])
68 checksum = digits.pop()
69 even_digits = digits[-1::-2]
70 odd_digits = digits[-2::-2]
71 checksum *= -1
72 checksum += 2 * sum(even_digits) + sum(odd_digits)
73 return checksum % 10 == 0