-
-
Notifications
You must be signed in to change notification settings - Fork 27
Implement firstname decoding. #253
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
Changes from all commits
117d33d
165b9ed
5e1cc0d
c4cc166
3cb77c7
0502339
7957f4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |||||||||||||||||||||||
| from datetime import datetime, timedelta | ||||||||||||||||||||||||
| from itertools import combinations | ||||||||||||||||||||||||
| from re import Pattern | ||||||||||||||||||||||||
| from typing import Any, Literal | ||||||||||||||||||||||||
| from typing import Any, Literal, cast | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from dateutil import parser as date_parser | ||||||||||||||||||||||||
| from slugify import slugify | ||||||||||||||||||||||||
|
|
@@ -82,7 +82,15 @@ | |||||||||||||||||||||||
| _OMOCODIA_SUBS_INDEXES_COMBINATIONS.append(list(combo)) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _DATA: dict[str, dict[str, list[dict[str, Any]]]] = get_indexed_data() | ||||||||||||||||||||||||
| _DATA: dict[str, Any] | None = None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _get_data() -> dict[str, Any]: | ||||||||||||||||||||||||
| global _DATA | ||||||||||||||||||||||||
| if _DATA is None: | ||||||||||||||||||||||||
| _DATA = get_indexed_data() | ||||||||||||||||||||||||
| return _DATA | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| CODICEFISCALE_RE: Pattern[str] = re.compile( | ||||||||||||||||||||||||
| r"^" | ||||||||||||||||||||||||
|
|
@@ -144,17 +152,18 @@ def _get_date( | |||||||||||||||||||||||
| def _get_birthplace( | ||||||||||||||||||||||||
| birthplace: str, | ||||||||||||||||||||||||
| birthdate: datetime | str | None = None, | ||||||||||||||||||||||||
| ) -> dict[str, dict[str, Any]] | None: | ||||||||||||||||||||||||
| ) -> dict[str, Any] | None: | ||||||||||||||||||||||||
| birthplace_unicode_slug = slugify(birthplace, allow_unicode=True) | ||||||||||||||||||||||||
| birthplace_slug = slugify(birthplace) | ||||||||||||||||||||||||
| birthplace_code = birthplace_slug.upper() | ||||||||||||||||||||||||
| birthplaces_options = _DATA["municipalities"].get( | ||||||||||||||||||||||||
| data = _get_data() | ||||||||||||||||||||||||
| birthplaces_options = data["municipalities"].get( | ||||||||||||||||||||||||
| birthplace_unicode_slug, | ||||||||||||||||||||||||
| _DATA["municipalities"].get( | ||||||||||||||||||||||||
| data["municipalities"].get( | ||||||||||||||||||||||||
| birthplace_slug, | ||||||||||||||||||||||||
| _DATA["countries"].get( | ||||||||||||||||||||||||
| data["countries"].get( | ||||||||||||||||||||||||
| birthplace_slug, | ||||||||||||||||||||||||
| _DATA["codes"].get( | ||||||||||||||||||||||||
| data["codes"].get( | ||||||||||||||||||||||||
| birthplace_code, | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
|
|
@@ -165,23 +174,23 @@ def _get_birthplace( | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| birthdate_date = _get_date(birthdate) | ||||||||||||||||||||||||
| if not birthdate_date: | ||||||||||||||||||||||||
| return birthplaces_options[0].copy() | ||||||||||||||||||||||||
| return cast(dict[str, Any], birthplaces_options[0].copy()) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # search birthplace that has been created before / deleted after birthdate | ||||||||||||||||||||||||
| for birthplace_option in birthplaces_options: | ||||||||||||||||||||||||
| date_created = _get_date(birthplace_option["date_created"]) or datetime.min | ||||||||||||||||||||||||
| date_deleted = _get_date(birthplace_option["date_deleted"]) or datetime.max | ||||||||||||||||||||||||
| # print(birthdate_date, date_created, date_deleted) | ||||||||||||||||||||||||
| if birthdate_date >= date_created and birthdate_date <= date_deleted: | ||||||||||||||||||||||||
| return birthplace_option.copy() | ||||||||||||||||||||||||
| return cast(dict[str, Any], birthplace_option.copy()) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return _get_birthplace_fallback(birthplaces_options, birthdate_date) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _get_birthplace_fallback( | ||||||||||||||||||||||||
| birthplaces_options: list[dict[str, Any]], | ||||||||||||||||||||||||
| birthdate_date: datetime, | ||||||||||||||||||||||||
| ) -> dict[str, dict[str, Any]] | None: | ||||||||||||||||||||||||
| ) -> dict[str, Any] | None: | ||||||||||||||||||||||||
| # avoid wrong birthplace code error when birthdate falls in | ||||||||||||||||||||||||
| # missing date-range in the data-source even if birthplace code is valid | ||||||||||||||||||||||||
| birthplaces_options_count = len(birthplaces_options) | ||||||||||||||||||||||||
|
|
@@ -280,6 +289,43 @@ def encode_firstname(firstname: str) -> str: | |||||||||||||||||||||||
| return firstname_code | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def decode_firstname( | ||||||||||||||||||||||||
| firstname_code: str, gender: Literal["m", "M", "f", "F"] | None = None | ||||||||||||||||||||||||
| ) -> list[str] | None: | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Decodes firstname code to possible italian first names. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Returns a list of possible names that encode to the given code. | ||||||||||||||||||||||||
| Only works for common italian names. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| :param firstname_code: The 3-character firstname code | ||||||||||||||||||||||||
| :type firstname_code: string | ||||||||||||||||||||||||
| :param gender: Optional gender filter ('M' or 'F') | ||||||||||||||||||||||||
| :type gender: string | None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| :returns: List of possible first names, or None if not found | ||||||||||||||||||||||||
| :rtype: list[str] | None | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| firstname_code_upper = firstname_code.upper() | ||||||||||||||||||||||||
| data = _get_data() | ||||||||||||||||||||||||
| names_by_gender = cast( | ||||||||||||||||||||||||
| dict[str, list[str]] | None, data["names"].get(firstname_code_upper) | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if not names_by_gender: | ||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if gender: | ||||||||||||||||||||||||
| gender_upper = gender.upper() | ||||||||||||||||||||||||
| if gender_upper in ("M", "F"): | ||||||||||||||||||||||||
| gender_names = names_by_gender.get(gender_upper, []) | ||||||||||||||||||||||||
| return gender_names if gender_names else None | ||||||||||||||||||||||||
|
Comment on lines
+318
to
+322
|
||||||||||||||||||||||||
| if gender: | |
| gender_upper = gender.upper() | |
| if gender_upper in ("M", "F"): | |
| gender_names = names_by_gender.get(gender_upper, []) | |
| return gender_names if gender_names else None | |
| if gender is not None: | |
| gender_upper = gender.upper() | |
| if gender_upper not in ("M", "F"): | |
| raise ValueError("[codicefiscale] 'gender' argument must be 'M' or 'F'") | |
| gender_names = names_by_gender.get(gender_upper, []) | |
| return gender_names if gender_names else None |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding # noqa: C901 suppresses the configured McCabe complexity check for decode(). Since this is a core public API, consider refactoring into a few small private helpers (e.g., parsing birthdate/gender, resolving birthplace, computing firstname options) so linting can stay enabled and the function becomes easier to test/maintain.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ | |
|
|
||
| import os | ||
| import sys | ||
| from datetime import datetime | ||
| from typing import Any | ||
|
|
||
| import fsutil | ||
|
|
@@ -33,23 +32,31 @@ def get_countries_data() -> Any: | |
| return deleted_countries + countries | ||
|
|
||
|
|
||
| def get_indexed_data() -> dict[ | ||
| str, dict[str, list[dict[str, bool | datetime | str | list[str]]]] | ||
| ]: | ||
| def get_names_data() -> Any: | ||
| names = get_data("names.json") | ||
| return names | ||
|
|
||
|
|
||
| def get_indexed_data() -> dict[str, Any]: | ||
| from codicefiscale.codicefiscale import encode_firstname | ||
|
|
||
| municipalities = get_municipalities_data() | ||
| countries = get_countries_data() | ||
| data: dict[str, dict[str, list[dict[str, bool | datetime | str | list[str]]]]] = { | ||
| names = get_names_data() | ||
|
|
||
| data: dict[str, Any] = { | ||
| "municipalities": {}, | ||
| "countries": {}, | ||
| "codes": {}, | ||
| "names": {}, | ||
| } | ||
|
Comment on lines
+40
to
52
|
||
|
|
||
| for municipality in municipalities: | ||
| code = municipality["code"] | ||
| province = municipality["province"].lower() | ||
| municipality_unicode_slug = slugify(municipality["name"], allow_unicode=True) | ||
| names = [municipality_unicode_slug] + municipality["name_slugs"] | ||
| for name in names: | ||
| municipality_names = [municipality_unicode_slug] + municipality["name_slugs"] | ||
| for name in municipality_names: | ||
| name_and_province = f"{name}-{province}" | ||
| data["municipalities"].setdefault(name, []) | ||
| data["municipalities"].setdefault(name_and_province, []) | ||
|
|
@@ -60,11 +67,21 @@ def get_indexed_data() -> dict[ | |
|
|
||
| for country in countries: | ||
| code = country["code"] | ||
| names = country["name_slugs"] | ||
| for name in names: | ||
| country_names = country["name_slugs"] | ||
| for name in country_names: | ||
| data["countries"].setdefault(name, []) | ||
| data["countries"][name].append(country) | ||
| data["codes"].setdefault(code, []) | ||
| data["codes"][code].append(country) | ||
|
|
||
| for gender, gender_names in names.items(): | ||
| for name in gender_names: | ||
| code = encode_firstname(name) | ||
| data["names"].setdefault(code, {"M": set(), "F": set()}) | ||
| data["names"][code][gender].add(name) | ||
|
|
||
| for code in data["names"]: | ||
| data["names"][code]["M"] = sorted(data["names"][code]["M"]) | ||
| data["names"][code]["F"] = sorted(data["names"][code]["F"]) | ||
|
|
||
| return data | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
decode_firstnamereturns the cached list from the global names index when a validgenderis provided. Because this is a mutable list shared across calls, a caller could accidentally mutate it (e.g.,append/sort) and corrupt subsequent results, includingdecode()output. Return a copy (e.g.,gender_names.copy()) to avoid leaking internal mutable state.