es-normalizer-py 0.2.4__py3-none-any.whl

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.
@@ -0,0 +1,243 @@
1
+ """
2
+ Contact data normalization adapted from the Empreinte Sociométrique's normalizers, the Python port of es-normalizer
3
+
4
+ Every normalizer takes a string and returns the normalized string, or `None` when nothing usable is left.
5
+ """
6
+
7
+ import re
8
+ import unicodedata
9
+ from typing import Callable, Optional
10
+
11
+ from .dictionary import Dictionary, address_dictionary, first_name_dictionary, get_set, title_dictionary
12
+
13
+ Normalizer = Callable[[str], Optional[str]]
14
+
15
+
16
+ def normalize(data: str, normalizer: Normalizer) -> Optional[str]:
17
+ return normalizer(data)
18
+
19
+
20
+ def uniformize(data: str) -> Optional[str]:
21
+ """
22
+ Upper-cases the passed string without its accents, any run of other characters than letters, digits and dashes becoming a single space
23
+ """
24
+ normalized = "".join(c for c in unicodedata.normalize("NFD", data.strip().lower()) if not "̀" <= c <= "ͯ")
25
+ if normalized == "":
26
+ return None
27
+ uniformized = normalized.replace("ß", "ss").replace("ø", "o")
28
+ uniformized = re.sub(r"[^a-zA-Z0-9-]+", "-", uniformized)
29
+ uniformized = re.sub(r"-+", "-", uniformized).replace("-", " ").strip().upper()
30
+ return uniformized or None
31
+
32
+
33
+ def Any(data: str) -> Optional[str]: # noqa: N802
34
+ """
35
+ Normalizer to use for any kind of data when no specific normalizer exists
36
+ """
37
+ return uniformize(data)
38
+
39
+
40
+ def AddressLine(transform_title: bool = False) -> Normalizer: # noqa: N802
41
+ """
42
+ Normalizer of an address line, eg. `8, rue Henner` becomes `8 RUE HENNER`; with `transform_title`, a French address
43
+ line 1 gets its title coded, eg. `Monsieur Cyril Dever` becomes `1 CYRIL DEVER`
44
+ """
45
+
46
+ def normalizer(data: str) -> Optional[str]:
47
+ uniformized = uniformize(data)
48
+ if uniformized is None:
49
+ return None
50
+ return (title_dictionary if transform_title else address_dictionary).translate_text(uniformized)
51
+
52
+ return normalizer
53
+
54
+
55
+ _AL6 = re.compile(r"^((0[1-9]|[1-8][0-9]|9[0-5]|2[AB]|97[1-8]|98[46-9])([0-9]{3}))(.*)$")
56
+
57
+
58
+ def AddressLine6(use_department_france: bool = True) -> Normalizer: # noqa: N802
59
+ """
60
+ Normalizer of the French address line 6, the département inserted between the postal code and the town, eg. `75009 75 PARIS`
61
+ """
62
+
63
+ def normalizer(data: str) -> Optional[str]:
64
+ line = AddressLine(False)(data)
65
+ if line is None:
66
+ return None
67
+ matches = _AL6.match(line)
68
+ if not matches or not matches.group(1):
69
+ return None
70
+ cp, dpt, city = matches.group(1), matches.group(2), matches.group(4)
71
+ if city.strip().startswith(dpt):
72
+ parts = [cp, city.strip()] if use_department_france else [cp, city.replace(dpt, "", 1).strip()]
73
+ else:
74
+ parts = [cp, dpt, city.strip()] if use_department_france else [cp, city.strip()]
75
+ return " ".join(parts).strip()
76
+
77
+ return normalizer
78
+
79
+
80
+ def City(data: str) -> Optional[str]: # noqa: N802
81
+ """
82
+ Normalizer of a town's name, its CEDEX mention removed
83
+ """
84
+ uniformized = uniformize(data)
85
+ if uniformized is None:
86
+ return None
87
+ found = re.sub(r"CDX(\s+[0-9]+)*", "", address_dictionary.translate_text(uniformized), count=1)
88
+ return re.sub(r"\s+", " ", found).strip()
89
+
90
+
91
+ _CP = re.compile(r"^((0[1-9][0-9]|[1-8][0-9]{2})|(9[0-5][0-9])|(2[AB][0-9])|(97[1-6]))[0-9]{2}$")
92
+ _CORSE = re.compile(r"^2[AB][0-9]{3}$")
93
+
94
+
95
+ def CodePostalFrance(data: str) -> Optional[str]: # noqa: N802
96
+ """
97
+ Normalizer of a French postal code, a Corsican one written with its département (`2A165`) becoming `20165`
98
+ """
99
+ uniformized = uniformize(data)
100
+ if uniformized is None:
101
+ return None
102
+ words = re.split(r"\s", uniformized)
103
+ candidate = words[0] if len(words) > 1 else uniformized
104
+ if not _CP.search(candidate):
105
+ return None
106
+ return re.sub(r"[AB]", "0", candidate, count=1) if _CORSE.search(candidate) else candidate
107
+
108
+
109
+ _DPT = re.compile(r"^(0[1-9]|[1-8][0-9]|9[0-5]|2[AB]|97[1-8]|98[46-9])([0-9]*)$")
110
+
111
+
112
+ def DepartementFrance(data: str) -> Optional[str]: # noqa: N802
113
+ """
114
+ Normalizer extracting the French département out of a postal code, eg. `2A` out of `20167`
115
+ """
116
+ uniformized = uniformize(data)
117
+ if not uniformized:
118
+ return None
119
+ matches = _DPT.match(uniformized)
120
+ if not matches or not matches.group(1):
121
+ return None
122
+ dpt = matches.group(1)
123
+ if dpt != "20":
124
+ return dpt
125
+ cp = CodePostalFrance(data)
126
+ if cp is None:
127
+ return None
128
+ return "2A" if 19999 < int(cp) < 20200 else "2B"
129
+
130
+
131
+ _EMAIL = re.compile(
132
+ r"^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f]))"
133
+ r"@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:[a-z]{2,}[a-z0-9-]*|([a-z][a-z0-9-]+))|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.?){4})$"
134
+ )
135
+
136
+
137
+ def Email(data: str) -> Optional[str]: # noqa: N802
138
+ """
139
+ Normalizer validating an e-mail address and lower-casing it
140
+ """
141
+ processed = data.strip().lower()
142
+ if len(processed) > 255 or not _EMAIL.search(processed):
143
+ return None
144
+ return processed
145
+
146
+
147
+ def FirstName(data: str) -> Optional[str]: # noqa: N802
148
+ """
149
+ Normalizer of a first name, through a French dictionary of first names, eg. `J Louis` becomes `JEAN LOUIS`
150
+ """
151
+ uniformized = uniformize(data)
152
+ return None if uniformized is None else first_name_dictionary.translate_word(uniformized)
153
+
154
+
155
+ _MOBILE = re.compile(r"^(((00)?(33))|0)?([0]?)([0-9])([0-9]{2})([0-9]{3})([0-9]{3})$")
156
+
157
+
158
+ def Mobile(data: str) -> Optional[str]: # noqa: N802
159
+ """
160
+ Normalizer of a French mobile phone number in the international format, eg. `+33 (0) 623 456 789`
161
+ """
162
+ uniformized = uniformize(data)
163
+ if uniformized is None:
164
+ return None
165
+ m = _MOBILE.match(re.sub(r"\s", "", uniformized))
166
+ if not m:
167
+ return None
168
+ if not (m.group(7) and m.group(8) and m.group(9)) or (m.group(7) + m.group(8) + m.group(9)).endswith("00000000"):
169
+ return None
170
+ if not ((m.group(6) and m.group(6) == "6") or m.group(6) == "7"):
171
+ return None
172
+ international = f"+{m.group(4)}" if m.group(4) else "+33"
173
+ prefix = f"({m.group(5)})" if m.group(5) else "(0)"
174
+ return " ".join([international, prefix, m.group(6) + m.group(7), m.group(8), m.group(9)])
175
+
176
+
177
+ _PHONE = re.compile(r"^(((00)?(33)|0?(262)|0?(590)|0?(594)|0?(596))([^1-9]*)|0)?([^0-9]*)([0-9]{3})([0-9]{3})([0-9]{3})$")
178
+
179
+
180
+ def PhoneNumber(data: str) -> Optional[str]: # noqa: N802
181
+ """
182
+ Normalizer of a French phone or fax number in the international format, eg. `+33 (0) 123 456 789`
183
+ """
184
+ uniformized = uniformize(data)
185
+ if uniformized is None:
186
+ return None
187
+ m = _PHONE.match(re.sub(r"\s", "", uniformized))
188
+ if not m:
189
+ return None
190
+ if not (m.group(11) and m.group(12) and m.group(13)) or (m.group(11) + m.group(12) + m.group(13)).endswith("00000000"):
191
+ return None
192
+ international = f"+{m.group(4)}" if m.group(4) else "+33"
193
+ prefix = f"({m.group(9)})" if m.group(9) else "(0)"
194
+ return " ".join([international, prefix, m.group(11), m.group(12), m.group(13)])
195
+
196
+
197
+ _STREET_NUMBER = re.compile(r"^([0-9]*)\s*(.*)$")
198
+
199
+
200
+ def StreetNumber(data: str) -> Optional[str]: # noqa: N802
201
+ """
202
+ Normalizer of a street number, eg. `1bis` or `1 bis` becomes `1B`
203
+ """
204
+ m = _STREET_NUMBER.match(data.strip())
205
+ if not m:
206
+ return None
207
+ if not m.group(2):
208
+ return m.group(1)
209
+ uniformized = uniformize(m.group(2))
210
+ if uniformized is None:
211
+ return None
212
+ return m.group(1) + address_dictionary.translate_text(uniformized).split(" ")[0]
213
+
214
+
215
+ _CODE_F, _CODE_M, _CODE_U = "2", "1", "0"
216
+
217
+
218
+ def Title(data: str) -> Optional[str]: # noqa: N802
219
+ """
220
+ Normalizer of a title into a code: 1 for gentlemen, 2 for ladies, 0 when undefined or unknown
221
+ """
222
+ translated = uniformize(data)
223
+ if translated is None:
224
+ return None
225
+ if translated in ("H", "M"):
226
+ return _CODE_M
227
+ if translated == "F":
228
+ return _CODE_F
229
+ if translated == "U":
230
+ return _CODE_U
231
+ found = re.findall(r"[0-9]+", title_dictionary.translate_text(translated))
232
+ if not found:
233
+ return None
234
+ return found[0] if len(found) == 1 else _CODE_U
235
+
236
+
237
+ from .date_of_birth import FRENCH_DATE, ISO_DATE, DateOfBirth, Milliseconds, Timestamp # noqa: E402
238
+
239
+ __all__ = [
240
+ "Dictionary", "get_set", "normalize", "uniformize", "Any", "AddressLine", "AddressLine6", "City", "CodePostalFrance",
241
+ "DateOfBirth", "DepartementFrance", "Email", "FirstName", "Mobile", "PhoneNumber", "StreetNumber", "Title",
242
+ "FRENCH_DATE", "ISO_DATE", "Timestamp", "Milliseconds",
243
+ ]
@@ -0,0 +1,128 @@
1
+ """
2
+ The date of birth normalizer, which reads and writes dates the way moment.js does in its forgiving mode, as es-normalizer relies on it
3
+ """
4
+
5
+ import calendar
6
+ import re
7
+ from datetime import date, datetime
8
+ from typing import Callable, Optional
9
+
10
+ FRENCH_DATE = "DD/MM/YYYY"
11
+ ISO_DATE = "YYYYMMDD"
12
+ Timestamp = "timestamp"
13
+ Milliseconds = "timestamp_millis"
14
+
15
+ _SEPARATORS = re.compile(r"[-:/\s]+")
16
+ _TOKENS = re.compile(r"YYYY|YY|MMMM|MMM|MM|M|DD|D|HH|H|SS|S")
17
+ _MONTHS = [calendar.month_name[i] for i in range(1, 13)]
18
+ _SHORT_MONTHS = [calendar.month_abbr[i] for i in range(1, 13)]
19
+ _MONTH_NAME = re.compile("|".join(_MONTHS + _SHORT_MONTHS), re.IGNORECASE)
20
+ # What each token matches, searched anywhere in what is left of the input
21
+ _PARSE = {
22
+ "YYYY": re.compile(r"\d{1,4}"),
23
+ "YY": re.compile(r"\d\d?"),
24
+ "MMMM": _MONTH_NAME,
25
+ "MMM": _MONTH_NAME,
26
+ "MM": re.compile(r"\d\d?"),
27
+ "M": re.compile(r"\d\d?"),
28
+ "DD": re.compile(r"\d\d?"),
29
+ "D": re.compile(r"\d\d?"),
30
+ "HH": re.compile(r"\d\d?"),
31
+ "H": re.compile(r"\d\d?"),
32
+ "SS": re.compile(r"\d+"),
33
+ "S": re.compile(r"\d+"),
34
+ }
35
+
36
+
37
+ def _month_of(name: str) -> int:
38
+ name = name.lower()
39
+ for i, (full, short) in enumerate(zip(_MONTHS, _SHORT_MONTHS), start=1):
40
+ if name == full.lower() or name == short.lower():
41
+ return i
42
+ return 0
43
+
44
+
45
+ def _two_digit_year(value: str) -> int:
46
+ return int(value) + (1900 if int(value) > 68 else 2000)
47
+
48
+
49
+ def _parse(data: str, fmt: str) -> Optional[date]:
50
+ year, month, day = date.today().year, 1, 1
51
+ rest = data
52
+ parsed = False
53
+ for token in _TOKENS.findall(fmt):
54
+ found = _PARSE[token].search(rest)
55
+ if not found:
56
+ continue
57
+ rest = rest[found.end():]
58
+ parsed = True
59
+ value = found.group(0)
60
+ if token == "YYYY":
61
+ # As moment.js: a year of two digits is read as one of `YY`
62
+ year = _two_digit_year(value) if len(value) == 2 else int(value)
63
+ elif token == "YY":
64
+ year = _two_digit_year(value)
65
+ elif token in ("MMMM", "MMM"):
66
+ month = _month_of(value)
67
+ elif token in ("MM", "M"):
68
+ month = int(value)
69
+ elif token in ("DD", "D"):
70
+ day = int(value)
71
+ if not parsed:
72
+ return None
73
+ try:
74
+ return date(year, month, day)
75
+ except ValueError:
76
+ return None
77
+
78
+
79
+ def _format(d: date, fmt: str) -> str:
80
+ def token(m: re.Match) -> str:
81
+ t = m.group(0)
82
+ return {
83
+ "YYYY": f"{d.year:04d}",
84
+ "YY": f"{d.year % 100:02d}",
85
+ "MMMM": _MONTHS[d.month - 1],
86
+ "MMM": _SHORT_MONTHS[d.month - 1],
87
+ "MM": f"{d.month:02d}",
88
+ "M": str(d.month),
89
+ "DD": f"{d.day:02d}",
90
+ "D": str(d.day),
91
+ "HH": "00",
92
+ "H": "0",
93
+ "SS": "00",
94
+ "S": "0",
95
+ }[t]
96
+
97
+ return _TOKENS.sub(token, fmt)
98
+
99
+
100
+ def _leading_int(data: str) -> Optional[int]:
101
+ # As JavaScript's `parseInt`: the leading digits, sign included
102
+ found = re.match(r"\s*([+-]?\d+)", data)
103
+ return int(found.group(1)) if found else None
104
+
105
+
106
+ def DateOfBirth(input_format: str = None, output_format: str = None) -> Callable[[str], Optional[str]]: # noqa: N802
107
+ """
108
+ Normalizer of a date of birth, read in the input format (ISO by default) and written in the output one (French by
109
+ default), eg. `24/04/2010` read as `DD/MM/YYYY` and written as `ISO_DATE` becomes `20100424`
110
+ """
111
+ inp = ISO_DATE if not input_format else input_format
112
+ out = FRENCH_DATE if not output_format else output_format
113
+
114
+ def normalizer(data: str) -> Optional[str]:
115
+ if inp.lower() in (Timestamp, Milliseconds):
116
+ value = _leading_int(data)
117
+ if value is None:
118
+ return None
119
+ d = datetime.fromtimestamp(value if inp.lower() == Timestamp else value / 1000).date()
120
+ else:
121
+ d = _parse(data, _SEPARATORS.sub("-", inp).upper())
122
+ if d is None:
123
+ return None
124
+ written = _format(d, _SEPARATORS.sub("-", out).upper())
125
+ separator = _SEPARATORS.search(out)
126
+ return written if separator is None else written.replace("-", separator.group(0))
127
+
128
+ return normalizer
@@ -0,0 +1,48 @@
1
+ from .address import ADDRESS
2
+ from .firstname import FIRST_NAME
3
+ from .title import TITLE
4
+
5
+
6
+ class Dictionary:
7
+ """
8
+ Translates words through a set of `word -> translation` pairs, an empty translation dropping the word; a key of several words translates them together, the longest first
9
+ """
10
+
11
+ def __init__(self, data_set: dict):
12
+ self.set = data_set
13
+ self.phrases = sorted((tuple(key.split(" ")) for key in data_set if " " in key), key=len, reverse=True)
14
+
15
+ def translate_text(self, text: str) -> str:
16
+ words = text.split(" ")
17
+ translated = []
18
+ i = 0
19
+ while i < len(words):
20
+ phrase = next((p for p in self.phrases if tuple(words[i : i + len(p)]) == p), None)
21
+ if phrase is not None and self.set[" ".join(phrase)] is not None:
22
+ translated.append(self.set[" ".join(phrase)])
23
+ i += len(phrase)
24
+ continue
25
+ translated.append(self.translate_word(words[i]))
26
+ i += 1
27
+ return " ".join(word for word in translated if word != "")
28
+
29
+ def translate_word(self, word: str) -> str:
30
+ translation = self.set.get(word)
31
+ return word if translation is None else translation
32
+
33
+
34
+ def get_set(data: str) -> dict:
35
+ """
36
+ Reads a dictionary written one `word<TAB>translation` pair per line; a line without a tab translates nothing
37
+ """
38
+ data_set = {}
39
+ for line in data.split("\n"):
40
+ # As JavaScript's `split('\t', 2)`: anything after a second tab is dropped
41
+ parts = line.split("\t")[:2]
42
+ data_set[parts[0]] = parts[1] if len(parts) > 1 else None
43
+ return data_set
44
+
45
+
46
+ address_dictionary = Dictionary(get_set(ADDRESS))
47
+ first_name_dictionary = Dictionary(get_set(FIRST_NAME))
48
+ title_dictionary = Dictionary(get_set(TITLE))
@@ -0,0 +1,110 @@
1
+ ADDRESS = """ALLEE ALL
2
+ ALLEES ALL
3
+ AVENUE AV
4
+ AVENU AV
5
+ AVE AV
6
+ AVN AV
7
+ BOUL BD
8
+ BOULEV BD
9
+ BOULEVARD BD
10
+ BOULEVAR BD
11
+ BLVD BD
12
+ BVD BD
13
+ BLD BD
14
+ BRD BD
15
+ CHE CH
16
+ CHEMIN CH
17
+ CHM CH
18
+ COUR CRS
19
+ COURS CRS
20
+ DOMAINE DOM
21
+ DOMAINES DOM
22
+ ESCALIER ESC
23
+ ESPLANADE ESP
24
+ IMPAS IMP
25
+ IMPASSE IMP
26
+ IMPASSES IMP
27
+ LOTISSEMENT LOT
28
+ LOTISSEMENTS LOT
29
+ PASSAGE PASS
30
+ PCE PL
31
+ PLACE PL
32
+ PLC PL
33
+ PORTE PTE
34
+ PRTE PTE
35
+ QUAI QU
36
+ QUAIS QU
37
+ RESIDENCE RES
38
+ RESIDENCES RES
39
+ ROND POINT RPT
40
+ RONDPOINT RPT
41
+ RDPT RPT
42
+ ROUTE RTE
43
+ ROUTES RTE
44
+ R RUE
45
+ RU RUE
46
+ RUELLE RLE
47
+ RUELLES RLE
48
+ RUES RUE
49
+ SENT SENTE
50
+ SENTE SENTE
51
+ SQUA SQ
52
+ SQUARE SQ
53
+ VILLA VLA
54
+ VILLAS VLA
55
+ VOIE VOI
56
+ VOIES VOI
57
+ BIS B
58
+ BI B
59
+ BS B
60
+ TER T
61
+ TR T
62
+ QUATER Q
63
+ QUAT Q
64
+ QUINQUIES C
65
+ QUI C
66
+ SEXIES S
67
+ SEX S
68
+ SEPTIES Z
69
+ SEP Z
70
+ OCTIES O
71
+ OCT O
72
+ NOVIES N
73
+ NOV N
74
+ DECIES D
75
+ DEC D
76
+ DU
77
+ DE
78
+ DES
79
+ D
80
+ LE
81
+ LA
82
+ LES
83
+ L
84
+ ET
85
+ GENERAL GAL
86
+ GNRL GAL
87
+ GL GAL
88
+ PRESIDENT PDT
89
+ MARECHAL MAL
90
+ DOCTEUR DR
91
+ GRANDE GDE
92
+ GRAND GD
93
+ PETITE PTE
94
+ PETIT PT
95
+ SAINT ST
96
+ SAINTE STE
97
+ CEDEX CDX
98
+ CEDX CDX
99
+ CDEX CDX
100
+ CX CDX
101
+ FAUBOURG FBG
102
+ CENTRE COMMERCIAL CCAL
103
+ CENTRE CIAL CCAL
104
+ CENTRE CCIAL CCAL
105
+ CTRE COMMERCIAL CCAL
106
+ CTRE CIAL CCAL
107
+ CTRE CCIAL CCAL
108
+ C COMMERCIAL CCAL
109
+ C CIAL CCAL
110
+ CCIAL CCAL"""
@@ -0,0 +1,229 @@
1
+ FIRST_NAME = """J ROGER JEAN ROGER
2
+ J ARNAUD JEAN ARNAUD
3
+ J AUGUSTE JEAN AUGUSTE
4
+ J SYLVESTRE JEAN SYLVESTRE
5
+ J SULLY JEAN SULLY
6
+ J GUY JEAN GUY
7
+ J MAXIME JEAN MAXIME
8
+ J RENE JEAN RENE
9
+ J MARTIN JEAN MARTIN
10
+ J ARMEL JEAN ARMEL
11
+ J ADRIEN JEAN ADRIEN
12
+ J MATTHIEU JEAN MATTHIEU
13
+ J GAEL JEAN GAEL
14
+ J RAOUL JEAN RAOUL
15
+ J STEPHAN JEAN STEPHAN
16
+ J VALERE JEAN VALERE
17
+ J NORBERT JEAN NORBERT
18
+ J YANNICK JEAN YANNICK
19
+ J FREDERIC JEAN FREDERIC
20
+ J EUDE JEAN EUDE
21
+ J AURELIEN JEAN AURELIEN
22
+ J YVON JEAN YVON
23
+ J LUCAS JEAN LUCAS
24
+ J THIERRY JEAN THIERRY
25
+ J MAX JEAN MAX
26
+ J LUDOVIC JEAN LUDOVIC
27
+ J ANDRE JEAN ANDRE
28
+ J ROCH JEAN ROCH
29
+ J ELIE JEAN ELIE
30
+ J ELOI JEAN ELOI
31
+ J SERGE JEAN SERGE
32
+ J PHILIPPE JEAN PHILIPPE
33
+ J DANY JEAN DANY
34
+ J MARIN JEAN MARIN
35
+ J YANN JEAN YANN
36
+ J HUGUES JEAN HUGUES
37
+ J CONSTANT JEAN CONSTANT
38
+ J JACK JEAN JACK
39
+ J ALIX JEAN ALIX
40
+ J MARK JEAN MARK
41
+ J CARL JEAN CARL
42
+ J GREGOIRE JEAN GREGOIRE
43
+ J EUGENE JEAN EUGENE
44
+ J BRIEUC JEAN BRIEUC
45
+ J DENYS JEAN DENYS
46
+ J EMILE JEAN EMILE
47
+ J NICOLAS JEAN NICOLAS
48
+ J FRANCOIS JEAN FRANCOIS
49
+ J RICHARD JEAN RICHARD
50
+ J THEOPHANE JEAN THEOPHANE
51
+ J DAVY JEAN DAVY
52
+ J BENOIT JEAN BENOIT
53
+ J PAUL JEAN PAUL
54
+ J RODOLPHE JEAN RODOLPHE
55
+ J QUENTIN JEAN QUENTIN
56
+ J MICKAEL JEAN MICKAEL
57
+ J BRIAC JEAN BRIAC
58
+ J DAVID JEAN DAVID
59
+ J MARC JEAN MARC
60
+ J VIANNEY JEAN VIANNEY
61
+ J AIME JEAN AIME
62
+ J DOMINIQUE JEAN DOMINIQUE
63
+ J EDDY JEAN EDDY
64
+ J BRICE JEAN BRICE
65
+ J FABIEN JEAN FABIEN
66
+ J CLAUDE JEAN CLAUDE
67
+ J WILFRIED JEAN WILFRIED
68
+ J DAMIEN JEAN DAMIEN
69
+ J WILLIAM JEAN WILLIAM
70
+ J EDOUARD JEAN EDOUARD
71
+ J RUDY JEAN RUDY
72
+ J LEON JEAN LEON
73
+ J NOEL JEAN NOEL
74
+ J BATISTE JEAN BATISTE
75
+ J REMY JEAN REMY
76
+ J OLIVIER JEAN OLIVIER
77
+ J BENJAMIN JEAN BENJAMIN
78
+ J MARY JEAN MARY
79
+ J BERNARD JEAN BERNARD
80
+ J CHARLES JEAN CHARLES
81
+ J CHRISTOPHER JEAN CHRISTOPHER
82
+ J MATHIS JEAN MATHIS
83
+ J MARCEL JEAN MARCEL
84
+ J CHRISTIAN JEAN CHRISTIAN
85
+ J STANISLAS JEAN STANISLAS
86
+ J ERNEST JEAN ERNEST
87
+ J BASTIEN JEAN BASTIEN
88
+ J FREDDY JEAN FREDDY
89
+ J ERICK JEAN ERICK
90
+ J VITAL JEAN VITAL
91
+ J FRANCO JEAN FRANCO
92
+ J ROMAIN JEAN ROMAIN
93
+ J RENAUD JEAN RENAUD
94
+ J PATRICE JEAN PATRICE
95
+ J PATRICK JEAN PATRICK
96
+ J EMMANUEL JEAN EMMANUEL
97
+ J STEPHANE JEAN STEPHANE
98
+ J XAVIER JEAN XAVIER
99
+ J BRUNO JEAN BRUNO
100
+ J MOISE JEAN MOISE
101
+ J LUC JEAN LUC
102
+ J VICTOR JEAN VICTOR
103
+ J VIVIEN JEAN VIVIEN
104
+ J CYRILLE JEAN CYRILLE
105
+ J KARL JEAN KARL
106
+ J JOSEPH JEAN JOSEPH
107
+ J MICHAEL JEAN MICHAEL
108
+ J AUGUSTIN JEAN AUGUSTIN
109
+ J THOMAS JEAN THOMAS
110
+ J LOUIS JEAN LOUIS
111
+ J ALEXANDRE JEAN ALEXANDRE
112
+ J SYLVAIN JEAN SYLVAIN
113
+ J LOIC JEAN LOIC
114
+ J PHILIP JEAN PHILIP
115
+ J FERNAND JEAN FERNAND
116
+ J MALO JEAN MALO
117
+ J ALBERT JEAN ALBERT
118
+ J FLORENT JEAN FLORENT
119
+ J POL JEAN POL
120
+ J ARMAND JEAN ARMAND
121
+ J GERARD JEAN GERARD
122
+ J AXEL JEAN AXEL
123
+ J FABRICE JEAN FABRICE
124
+ J ARTHUR JEAN ARTHUR
125
+ J LUCIEN JEAN LUCIEN
126
+ J LAURENT JEAN LAURENT
127
+ J JUNIOR JEAN JUNIOR
128
+ J LUCE JEAN LUCE
129
+ J CLEMENT JEAN CLEMENT
130
+ J COME JEAN COME
131
+ J VALERY JEAN VALERY
132
+ J HERVE JEAN HERVE
133
+ J BART JEAN BART
134
+ J FELIX JEAN FELIX
135
+ J REMI JEAN REMI
136
+ J CYRIL JEAN CYRIL
137
+ J MATHIAS JEAN MATHIAS
138
+ J BLAISE JEAN BLAISE
139
+ J MARIE JEAN MARIE
140
+ J CAMILLE JEAN CAMILLE
141
+ J LOU JEAN LOU
142
+ J DANIEL JEAN DANIEL
143
+ J JACQUES JEAN JACQUES
144
+ J FLORIAN JEAN FLORIAN
145
+ J GUILLAUME JEAN GUILLAUME
146
+ J SEBASTIEN JEAN SEBASTIEN
147
+ J HENRI JEAN HENRI
148
+ J CYPRIEN JEAN CYPRIEN
149
+ J LEO JEAN LEO
150
+ J ERIC JEAN ERIC
151
+ J SAMUEL JEAN SAMUEL
152
+ J GILLES JEAN GILLES
153
+ J ETIENNE JEAN ETIENNE
154
+ J VINCENT JEAN VINCENT
155
+ J ROLAND JEAN ROLAND
156
+ J EUDES JEAN EUDES
157
+ J JOEL JEAN JOEL
158
+ J GIL JEAN GIL
159
+ J TRISTAN JEAN TRISTAN
160
+ J ALEXIS JEAN ALEXIS
161
+ J CHRISTOPHE JEAN CHRISTOPHE
162
+ J BOSCO JEAN BOSCO
163
+ J DESIRE JEAN DESIRE
164
+ J LOUP JEAN LOUP
165
+ J PIERROT JEAN PIERROT
166
+ J GREGORY JEAN GREGORY
167
+ J KEVIN JEAN KEVIN
168
+ J HUGO JEAN HUGO
169
+ J YAN JEAN YAN
170
+ J BAPTISTE JEAN BAPTISTE
171
+ J JULES JEAN JULES
172
+ J ROBERT JEAN ROBERT
173
+ J GUILHEM JEAN GUILHEM
174
+ J JULIEN JEAN JULIEN
175
+ J MATHIEU JEAN MATHIEU
176
+ J ALEX JEAN ALEX
177
+ J PIERRE JEAN PIERRE
178
+ J GILBERT JEAN GILBERT
179
+ J GEORGES JEAN GEORGES
180
+ J HENRY JEAN HENRY
181
+ J WILFRID JEAN WILFRID
182
+ J LIN JEAN LIN
183
+ J BARTHELEMY JEAN BARTHELEMY
184
+ J ANTOINE JEAN ANTOINE
185
+ J EDMOND JEAN EDMOND
186
+ J ALAIN JEAN ALAIN
187
+ J DENIS JEAN DENIS
188
+ J GERALD JEAN GERALD
189
+ J WILSON JEAN WILSON
190
+ J LIONEL JEAN LIONEL
191
+ J RAPHAEL JEAN RAPHAEL
192
+ J ROBIN JEAN ROBIN
193
+ J YVES JEAN YVES
194
+ J PASCAL JEAN PASCAL
195
+ J FRANCIS JEAN FRANCIS
196
+ J TEDDY JEAN TEDDY
197
+ J AYMERIC JEAN AYMERIC
198
+ J GUSTAVE JEAN GUSTAVE
199
+ J MAEL JEAN MAEL
200
+ J ALFRED JEAN ALFRED
201
+ J MARIUS JEAN MARIUS
202
+ J REGIS JEAN REGIS
203
+ J ABEL JEAN ABEL
204
+ J HUBERT JEAN HUBERT
205
+ J KARIM JEAN KARIM
206
+ J CEDRIC JEAN CEDRIC
207
+ J NOE JEAN NOE
208
+ J FRANCK JEAN FRANCK
209
+ J JEROME JEAN JEROME
210
+ J SIMON JEAN SIMON
211
+ J EMILIEN JEAN EMILIEN
212
+ J MAXENCE JEAN MAXENCE
213
+ J FRED JEAN FRED
214
+ J VALENTIN JEAN VALENTIN
215
+ J JOSE JEAN JOSE
216
+ J MICHEL JEAN MICHEL
217
+ J RAYMOND JEAN RAYMOND
218
+ J MARTIAL JEAN MARTIAL
219
+ J MIKAEL JEAN MIKAEL
220
+ J ANTHONY JEAN ANTHONY
221
+ J CLAIR JEAN CLAIR
222
+ J TOUSSAINT JEAN TOUSSAINT
223
+ J WILLY JEAN WILLY
224
+ J FERDINAND JEAN FERDINAND
225
+ J GABRIEL JEAN GABRIEL
226
+ J MAURICE JEAN MAURICE
227
+ J DIDIER JEAN DIDIER
228
+ J BERTRAND JEAN BERTRAND
229
+ J GASTON JEAN GASTON"""
@@ -0,0 +1,41 @@
1
+ TITLE = """MADAME 2
2
+ MME 2
3
+ MESDAMES 2
4
+ MMES 2
5
+ MADEMOISELLE 2
6
+ MLLE 2
7
+ MESDEMOISELLES 2
8
+ MLLES 2
9
+ MONSIEUR 1
10
+ MR 1
11
+ M 1
12
+ MESSIEURS 1
13
+ MM 1
14
+ VEUVE 2
15
+ VVE 2
16
+ DOCTEUR 0
17
+ DR 0
18
+ DOCTEURS 0
19
+ DRS 0
20
+ PROFESSEUR 0
21
+ PR 0
22
+ PROFESSEURS 0
23
+ PRS 0
24
+ MAITRE 0
25
+ ME 0
26
+ MAITRES 0
27
+ MES 0
28
+ MONSEIGNEUR 1
29
+ MGR 1
30
+ GENERAL 0
31
+ GAL 0
32
+ GNRL 0
33
+ MARECHAL 0
34
+ COLONEL 0
35
+ CEL 0
36
+ CAPITAINE 0
37
+ CNE 0
38
+ COMMANDANT 0
39
+ CDT 0
40
+ LIEUTENANT 0
41
+ LT 0"""
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: es-normalizer-py
3
+ Version: 0.2.4
4
+ Summary: Contact data normalization adapted from the Empreinte Sociométrique's normalizers, the Python port of es-normalizer
5
+ Author-email: Cyril Dever <cdever@pep-s.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cyrildever/es-normalizer-py
8
+ Keywords: data,contact,normalization,empreinte sociometrique
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10.2
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # es-normalizer-py
17
+ _Contact data normalization adapted from the Empreinte Sociométrique's normalizers (in Python)_
18
+
19
+ ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/cyrildever/es-normalizer-py)
20
+ ![GitHub last commit](https://img.shields.io/github/last-commit/cyrildever/es-normalizer-py)
21
+ ![GitHub issues](https://img.shields.io/github/issues/cyrildever/es-normalizer-py)
22
+ ![GitHub license](https://img.shields.io/github/license/cyrildever/es-normalizer-py)
23
+ ![PyPI - Version](https://img.shields.io/pypi/v/es-normalizer-py)
24
+
25
+ This is the Python port of [es-normalizer](https://github.com/cyrildever/es-normalizer), the TypeScript normalization library for contact data (address, phones, etc.) adapted from [Edgewhere](https://www.edgewhere.fr)'s Empreinte Sociométrique patented normalizers.
26
+
27
+ ### Motivation
28
+
29
+ When it comes to hashing data, the necessary uniqueness of the source sometimes makes it hard to compare two hashed data, mostly if dealing with postal address. This library helps normalizing any contact data (the postal or e-mail address, the mobile or landline phone number, the title, names or date of birth of an individual) that would feed any hashing algorithm to make hash comparison always trustable.
30
+ It's based on the work for the Empreinte Sociométrique&trade; patented by Cyril Dever for Edgewhere.
31
+
32
+ ### TypeScript version
33
+
34
+ The TypeScript library lives at [cyrildever/es-normalizer](https://github.com/cyrildever/es-normalizer) and is published on npm as `es-normalizer`.
35
+ Version `0.2.4` of this port matches its version `2.4.0`: both give the same outputs for the same inputs, the test suite of es-normalizer is ported here case by case, and their dictionaries are the same.
36
+ An address dictionary key may hold several words (eg. `CENTRE COMMERCIAL`), translated together before the single words, the longest first.
37
+
38
+ ### Usage
39
+
40
+ ```
41
+ pip install es-normalizer-py
42
+ ```
43
+
44
+ *IMPORTANT*: as of this version, most normalizers are for use on French data.
45
+
46
+ To get a normalized string, pass the data and a normalizer function to `normalize()`: it returns the normalized string, or `None` when nothing usable is left.
47
+ There are currently eleven specific normalizer functions and a generic one:
48
+ * `Any`: the generic normalizer should be used if no specific normalizer exists;
49
+ * `AddressLine`: pass any address line through it to get a normalized address, eg. `8, rue Henner` becomes `8 RUE HENNER`, and eventually `True` as parameter when trying to normalize a French address line 1 to also transform the title, eg. `Monsieur Cyril Dever` should then become `1 CYRIL DEVER` when passed `True` instead of `MONSIEUR CYRIL DEVER` for the default behaviour;
50
+ * `AddressLine6`: the French address line 6 with the department inserted in between the zip code and the city as in the Empreinte Sociométrique implementation (eg. `75009 75 PARIS`);
51
+ * `City`: for normalizing city names (it removes any Cedex mention in French address, for instance);
52
+ * `CodePostalFrance`: for French zip code;
53
+ * `DateOfBirth`: pass a date and up to two parameters: the input format (eg. `YYYY-MM-DD`) and the output format wished (eg. `ISO_DATE`), the default values being respectively the ISO format and the French date format;
54
+ * `DepartementFrance`: extract the French departement number (out of a code postal, for instance);
55
+ * `Email`: validates the passed e-mail and returns it in lower-case;
56
+ * `FirstName`: pass a first name and get a normalized one (it uses an enlarged French dictionay of first names to process it making it possible to pass from `"J.-Louis"` to `"JEAN LOUIS"`);
57
+ * `Mobile`: to validate a French mobile phone;
58
+ * `PhoneNumber`: to normalize a French phone or fax number in the international format, eg. `+33 (0) 123 456 789`;
59
+ * `StreetNumber`: parses the passed field to normalize it the Empreinte Sociométrique&trade;'s way, eg. `1bis` or `1 bis` becomes `1B`;
60
+ * `Title`: returns a code depending on the passed string (1 for gentlemen, 2 for ladies, 0 when undefined or unknown).
61
+
62
+ ```python
63
+ from es_normalizer import *
64
+
65
+ print(normalize('#This is any String(). ', Any))
66
+ # THIS IS ANY STRING
67
+
68
+ print(normalize('Monsieur Cyril Dever', AddressLine(True)))
69
+ # 1 CYRIL DEVER
70
+
71
+ print(normalize('24, rué de Maubeuge', AddressLine()))
72
+ # 24 RUE MAUBEUGE
73
+
74
+ print(normalize('75009 .# Paris', AddressLine6()))
75
+ # 75009 75 PARIS
76
+
77
+ print(normalize('75009 .# Paris', AddressLine6(False)))
78
+ # 75009 PARIS
79
+
80
+ print(normalize('Paris Cedex 09', City))
81
+ # PARIS
82
+
83
+ print(normalize('70/12/01', DateOfBirth('YY/MM/DD', FRENCH_DATE)))
84
+ # 01/12/1970
85
+
86
+ print(normalize(' 75009 ', DepartementFrance))
87
+ # 75
88
+
89
+ print(normalize(' Contact@GMAIL.com', Email))
90
+ # contact@gmail.com
91
+ ```
92
+
93
+ `DateOfBirth` reads dates the way the forgiving parser of [moment.js](https://momentjs.com/) does, which es-normalizer relies on. Both give the same date for an input that fits its format; for an input that does not, eg. a numeric date read with a month name format (`DD MMM YYYY`), the guess of each may differ.
94
+
95
+ ### Tests
96
+
97
+ ```console
98
+ $ git clone https://github.com/cyrildever/es-normalizer-py.git
99
+ $ cd es-normalizer-py/
100
+ $ pip install -e .
101
+ $ python3 -m unittest discover
102
+ ```
103
+
104
+
105
+ ### License
106
+
107
+ This module is distributed under a MIT license. \
108
+ See the [LICENSE](LICENSE) file.
109
+
110
+
111
+ <hr />
112
+ &copy; 2026 Cyril Dever. All rights reserved.
@@ -0,0 +1,11 @@
1
+ es_normalizer/__init__.py,sha256=tVXR6T2qo0Y9PoNy2mXbAlSdLoRU9Nf4ROpK2fRRzoo,8661
2
+ es_normalizer/date_of_birth.py,sha256=6beiq5KDvq_Gz8HL3nB6YYCuqxsLDPTeiIhSvfHzeFE,4237
3
+ es_normalizer/dictionary/__init__.py,sha256=RFC6a90ZzfstjRctQPNG1bf3A8LEqGY6v3GnpyXnZsc,1794
4
+ es_normalizer/dictionary/address.py,sha256=-zhoG_Gov3Bm5tOsRX_xVfk7HUKDenUvIIlvXPkg1GE,1089
5
+ es_normalizer/dictionary/firstname.py,sha256=5EYDLA1vcM1gnO3TQeUI6ss6Rs3l5oHqQMkePmZYQOY,4790
6
+ es_normalizer/dictionary/title.py,sha256=kCMb6w48AysAPoLq_x5g-hdfzfIvoUh3PdZysB9MYKI,370
7
+ es_normalizer_py-0.2.4.dist-info/licenses/LICENSE,sha256=PM4uNGV9-SG80q1m93WGtx-FQooSUK18WonpzGc8mJI,1068
8
+ es_normalizer_py-0.2.4.dist-info/METADATA,sha256=u_YPjKFpxKmI-fcNl1b7nz2SQ-7hfiBGFfl7RAz72O0,5878
9
+ es_normalizer_py-0.2.4.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
10
+ es_normalizer_py-0.2.4.dist-info/top_level.txt,sha256=cw6hHhElZgvzvBNuYLouQa8qliIEtZDE0MkB1MzOqQc,14
11
+ es_normalizer_py-0.2.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyril Dever
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ es_normalizer