mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
feat(ip-info): return timezone and add a required-field filter
get_ip_info(require=...) skips a geolocation API whose result lacks a required field, so ipinfo Lite falls through to ipinfo.io or ip-api.in.
This commit is contained in:
@@ -80,3 +80,18 @@ def test_get_ip_info_collects_connection_errors_only(monkeypatch):
|
||||
errors: list[Exception] = []
|
||||
assert ip_info.get_ip_info(SimpleNamespace(proxies={"all": "http://127.0.0.1:9"}), errors=errors) is None
|
||||
assert [str(e) for e in errors] == ["refused"]
|
||||
|
||||
|
||||
def test_require_skips_provider_missing_field(monkeypatch):
|
||||
lite = {"country": "ca", "timezone": ""}
|
||||
full = {"country": "ca", "timezone": "America/Edmonton"}
|
||||
monkeypatch.setattr(
|
||||
ip_info, "build_providers", lambda: [("lite", lambda s: dict(lite)), ("full", lambda s: dict(full))]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ip_info,
|
||||
"Cacher",
|
||||
lambda *a: SimpleNamespace(get=lambda k: SimpleNamespace(data=None, expired=True, set=lambda *a, **k: None)),
|
||||
)
|
||||
assert ip_info.get_ip_info(object())["_provider"] == "lite"
|
||||
assert ip_info.get_ip_info(object(), require=("timezone",))["timezone"] == "America/Edmonton"
|
||||
|
||||
@@ -37,6 +37,7 @@ def normalize(
|
||||
asn: str = "",
|
||||
as_name: str = "",
|
||||
continent_code: str = "",
|
||||
timezone: str = "",
|
||||
) -> Optional[dict]:
|
||||
"""Assemble the canonical IP-info dict, or None if no country code is present."""
|
||||
code = country_code.strip()
|
||||
@@ -52,6 +53,7 @@ def normalize(
|
||||
"asn": asn,
|
||||
"as_name": as_name,
|
||||
"continent_code": continent_code.upper(),
|
||||
"timezone": timezone,
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +77,7 @@ def parse_ipinfo(data: dict) -> Optional[dict]:
|
||||
region=data.get("region") or "",
|
||||
city=data.get("city") or "",
|
||||
org=data.get("org") or "",
|
||||
timezone=data.get("timezone") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -90,6 +93,7 @@ def parse_ip_api_in(data: dict) -> Optional[dict]:
|
||||
asn=asn,
|
||||
as_name=org_name,
|
||||
continent_code=data.get("continent_code") or "",
|
||||
timezone=data.get("timezone") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -187,14 +191,16 @@ def get_ip_info(
|
||||
*,
|
||||
cached: bool = False,
|
||||
errors: Optional[list[Exception]] = None,
|
||||
require: tuple[str, ...] = (),
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Look up IP/geolocation info through ipinfo.io (Lite when `ipinfo_api_key` configured)
|
||||
with fallback to ip-api.in.
|
||||
|
||||
Live lookups return a dict with `ip`, `country` (lowercase ISO2), `country_code`
|
||||
(uppercase ISO2), `region`, `city`, `org`, `asn`, `as_name`, `continent_code` and
|
||||
`_provider`. Cached lookups return only `ip`/`country`/`country_code` (see GEO_CACHE_KEYS).
|
||||
(uppercase ISO2), `region`, `city`, `org`, `asn`, `as_name`, `continent_code`,
|
||||
`timezone` (IANA name, empty from ipinfo Lite) and `_provider`. Cached lookups return
|
||||
only `ip`/`country`/`country_code` (see GEO_CACHE_KEYS).
|
||||
Returns None if every geolocation API fails.
|
||||
|
||||
Args:
|
||||
@@ -206,6 +212,8 @@ def get_ip_info(
|
||||
local IP lookups, never with a proxied HTTP session.
|
||||
errors: When given, each geolocation API that raised an exception adds it to
|
||||
this list. A 429 or an unusable response adds nothing.
|
||||
require: Fields a live result must have. A geolocation API whose result has
|
||||
an empty required field is skipped, and the next API is tried.
|
||||
"""
|
||||
cache = None
|
||||
if cached:
|
||||
@@ -242,7 +250,7 @@ def get_ip_info(
|
||||
errors.append(e)
|
||||
continue
|
||||
|
||||
if not normalized:
|
||||
if not normalized or not all(normalized.get(k) for k in require):
|
||||
log.debug(f"Provider {name} returned no usable data")
|
||||
continue
|
||||
|
||||
|
||||
Reference in New Issue
Block a user