55from datetime import datetime , timedelta
66from itertools import combinations
77from re import Pattern
8- from typing import Any , Literal
8+ from typing import Any , Literal , cast
99
1010from dateutil import parser as date_parser
1111from slugify import slugify
8282 _OMOCODIA_SUBS_INDEXES_COMBINATIONS .append (list (combo ))
8383
8484
85- _DATA : dict [str , dict [str , list [dict [str , Any ]]]] = get_indexed_data ()
85+ _DATA : dict [str , Any ] | None = None
86+
87+
88+ def _get_data () -> dict [str , Any ]:
89+ global _DATA
90+ if _DATA is None :
91+ _DATA = get_indexed_data ()
92+ return _DATA
93+
8694
8795CODICEFISCALE_RE : Pattern [str ] = re .compile (
8896 r"^"
@@ -144,17 +152,18 @@ def _get_date(
144152def _get_birthplace (
145153 birthplace : str ,
146154 birthdate : datetime | str | None = None ,
147- ) -> dict [str , dict [ str , Any ] ] | None :
155+ ) -> dict [str , Any ] | None :
148156 birthplace_unicode_slug = slugify (birthplace , allow_unicode = True )
149157 birthplace_slug = slugify (birthplace )
150158 birthplace_code = birthplace_slug .upper ()
151- birthplaces_options = _DATA ["municipalities" ].get (
159+ data = _get_data ()
160+ birthplaces_options = data ["municipalities" ].get (
152161 birthplace_unicode_slug ,
153- _DATA ["municipalities" ].get (
162+ data ["municipalities" ].get (
154163 birthplace_slug ,
155- _DATA ["countries" ].get (
164+ data ["countries" ].get (
156165 birthplace_slug ,
157- _DATA ["codes" ].get (
166+ data ["codes" ].get (
158167 birthplace_code ,
159168 ),
160169 ),
@@ -165,23 +174,23 @@ def _get_birthplace(
165174
166175 birthdate_date = _get_date (birthdate )
167176 if not birthdate_date :
168- return birthplaces_options [0 ].copy ()
177+ return cast ( dict [ str , Any ], birthplaces_options [0 ].copy () )
169178
170179 # search birthplace that has been created before / deleted after birthdate
171180 for birthplace_option in birthplaces_options :
172181 date_created = _get_date (birthplace_option ["date_created" ]) or datetime .min
173182 date_deleted = _get_date (birthplace_option ["date_deleted" ]) or datetime .max
174183 # print(birthdate_date, date_created, date_deleted)
175184 if birthdate_date >= date_created and birthdate_date <= date_deleted :
176- return birthplace_option .copy ()
185+ return cast ( dict [ str , Any ], birthplace_option .copy () )
177186
178187 return _get_birthplace_fallback (birthplaces_options , birthdate_date )
179188
180189
181190def _get_birthplace_fallback (
182191 birthplaces_options : list [dict [str , Any ]],
183192 birthdate_date : datetime ,
184- ) -> dict [str , dict [ str , Any ] ] | None :
193+ ) -> dict [str , Any ] | None :
185194 # avoid wrong birthplace code error when birthdate falls in
186195 # missing date-range in the data-source even if birthplace code is valid
187196 birthplaces_options_count = len (birthplaces_options )
@@ -280,6 +289,43 @@ def encode_firstname(firstname: str) -> str:
280289 return firstname_code
281290
282291
292+ def decode_firstname (
293+ firstname_code : str , gender : Literal ["m" , "M" , "f" , "F" ] | None = None
294+ ) -> list [str ] | None :
295+ """
296+ Decodes firstname code to possible italian first names.
297+
298+ Returns a list of possible names that encode to the given code.
299+ Only works for common italian names.
300+
301+ :param firstname_code: The 3-character firstname code
302+ :type firstname_code: string
303+ :param gender: Optional gender filter ('M' or 'F')
304+ :type gender: string | None
305+
306+ :returns: List of possible first names, or None if not found
307+ :rtype: list[str] | None
308+ """
309+ firstname_code_upper = firstname_code .upper ()
310+ data = _get_data ()
311+ names_by_gender = cast (
312+ dict [str , list [str ]] | None , data ["names" ].get (firstname_code_upper )
313+ )
314+
315+ if not names_by_gender :
316+ return None
317+
318+ if gender :
319+ gender_upper = gender .upper ()
320+ if gender_upper in ("M" , "F" ):
321+ gender_names = names_by_gender .get (gender_upper , [])
322+ return gender_names if gender_names else None
323+
324+ # return all names (both genders) if no gender specified
325+ all_names = names_by_gender .get ("M" , []) + names_by_gender .get ("F" , [])
326+ return sorted (set (all_names )) if all_names else None
327+
328+
283329def encode_birthdate (
284330 birthdate : datetime | str | None ,
285331 gender : Literal ["m" , "M" , "f" , "F" ],
@@ -448,7 +494,7 @@ def decode_raw(code: str) -> dict[str, str]:
448494 return data
449495
450496
451- def decode (code : str ) -> dict [str , Any ]:
497+ def decode (code : str ) -> dict [str , Any ]: # noqa: C901
452498 """
453499 Decodes the italian fiscal code.
454500
@@ -466,11 +512,10 @@ def decode(code: str) -> dict[str, Any]:
466512 birthdate_month = _MONTHS .index (raw ["birthdate_month" ]) + 1
467513 birthdate_day = int (raw ["birthdate_day" ].translate (_OMOCODIA_DECODE_TRANS ))
468514
515+ gender : Literal ["M" , "F" ] = "M"
469516 if birthdate_day > 40 :
470517 birthdate_day -= 40
471518 gender = "F"
472- else :
473- gender = "M"
474519
475520 current_year = datetime .now ().year
476521 current_year_century_prefix = str (current_year )[0 :- 2 ]
@@ -517,12 +562,19 @@ def decode(code: str) -> dict[str, Any]:
517562 f"expected { cin_check !r} , found { cin !r} "
518563 )
519564
565+ # add possible first names if birthplace is in Italy (not foreign country)
566+ firstname_options = None
567+ is_foreign = birthplace and birthplace .get ("province" ) == "EE"
568+ if not is_foreign :
569+ firstname_options = decode_firstname (raw ["firstname" ], gender )
570+
520571 data = {
521572 "code" : code ,
522573 "omocodes" : _get_omocodes (code ),
523574 "gender" : gender ,
524575 "birthdate" : birthdate ,
525576 "birthplace" : birthplace ,
577+ "firstname_options" : firstname_options or [],
526578 "raw" : raw ,
527579 }
528580
0 commit comments