Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ def __init__(
field_name: str | None = None,
is_flag: bool | None = None,
# ... other keyword-only parameters
**_: Any, # ignore unknown keys
**_: object, # ignore unknown keys
) -> None:
```

Key points:
- Use `*` to enforce keyword-only arguments
- Accept `**_: Any` to ignore unknown keys from the API (forward compatibility)
- Accept `**_: object` to ignore unknown keys from the API (forward compatibility)
- Use `| None = None` for optional parameters
- Boolean fields can be `None` if not provided by API

Expand Down Expand Up @@ -252,7 +252,7 @@ def test_email(self) -> None:
*,
new_field: str | None = None,
# ... other params
**_: Any,
**_: object,
) -> None:
```

Expand Down Expand Up @@ -281,7 +281,7 @@ When creating a new model class:
2. **Follow the constructor pattern** from existing models
3. **Use type hints** for all attributes
4. **Use keyword-only arguments** with `*` separator
5. **Accept `**_: Any`** to ignore unknown API keys
5. **Accept `**_: object`** to ignore unknown API keys
6. **Provide comprehensive docstrings** for all attributes
7. **Add corresponding tests** with full coverage

Expand Down Expand Up @@ -376,7 +376,7 @@ Adding required parameters breaks existing code.
- Always add new parameters as optional with defaults
- Use keyword-only arguments (after `*`)
- Never add required positional parameters to existing constructors
- Use `**_: Any` to silently accept unknown parameters from API
- Use `**_: object` to silently accept unknown parameters from API

## Code Style Requirements

Expand Down
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ History
``aiohttp.encode_basic_auth()`` instead of the ``aiohttp.BasicAuth`` /
``auth=`` parameter, which are deprecated as of aiohttp 3.14.0. As a result,
the minimum required ``aiohttp`` version is now 3.14.0.
* The ``ip_address.anonymizer`` object may now contain a ``residential``
attribute. This is a ``geoip2.records.AnonymizerFeed`` object containing
residential proxy data for the network. It has the following fields:
``confidence``, ``network_last_seen``, and ``provider_name``. It may be
populated even when no other ``anonymizer`` attributes are set.

3.2.0 (2025-11-20)
++++++++++++++++++
Expand Down
2 changes: 1 addition & 1 deletion lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# lychee './**/*.md' './**/*.rst' './src/minfraud/**/*.py' './pyproject.toml'

# Include URL fragments in checks
include_fragments = true
include_fragments = "full"

# Don't allow any redirects, so links that have moved are surfaced and can be
# updated to their canonical destination.
Expand Down
32 changes: 19 additions & 13 deletions mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [
dependencies = [
"aiohttp>=3.14.0,<4.0.0",
"email_validator>=2.0.0,<3.0.0",
"geoip2>=5.2.0,<6.0.0",
"geoip2>=5.3.0,<6.0.0",
"requests>=2.24.0,<3.0.0",
"typing-extensions>=4.13.2",
"voluptuous",
Expand Down Expand Up @@ -92,8 +92,7 @@ ignorelist = ["id"]

[tool.ruff.lint.per-file-ignores]
"docs/*" = ["ALL"]
"src/minfraud/models.py" = ["ANN401", "PLR0913"]
"src/minfraud/webservice.py" = ["ANN401"]
"src/minfraud/models.py" = ["PLR0913"]
"tests/*" = ["ANN201", "D"]

[tool.tox]
Expand Down
36 changes: 18 additions & 18 deletions src/minfraud/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class GeoIP2Location(geoip2.records.Location):
`RFC 3339 <https://datatracker.ietf.org/doc/html/rfc3339>`_. For instance, the
local time in Boston might be returned as 2015-04-27T19:17:24-04:00."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
"""Initialize a GeoIP2Location instance."""
self.local_time = kwargs.get("local_time")
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -135,7 +135,7 @@ def __init__(
location: dict[str, Any] | None = None,
risk: float | None = None,
risk_reasons: list[dict[str, Any]] | None = None,
**kwargs: Any,
**kwargs: Any, # noqa: ANN401
) -> None:
"""Initialize an IPAddress instance."""
# For raw attribute
Expand All @@ -161,7 +161,7 @@ class ScoreIPAddress(_Serializable):
"""This field contains the risk associated with the IP address. The value
ranges from 0.01 to 99. A higher score indicates a higher risk."""

def __init__(self, *, risk: float | None = None, **_: Any) -> None:
def __init__(self, *, risk: float | None = None, **_: object) -> None:
"""Initialize a ScoreIPAddress instance."""
self.risk = risk

Expand Down Expand Up @@ -197,7 +197,7 @@ def __init__(
matches_provided_name: bool | None = None,
phone_number: str | None = None,
matches_provided_phone_number: bool | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize an Issuer instance."""
self.name = name
Expand Down Expand Up @@ -239,7 +239,7 @@ def __init__(
id: str | None = None,
last_seen: str | None = None,
local_time: str | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Device instance."""
self.confidence = confidence
Expand Down Expand Up @@ -277,7 +277,7 @@ def __init__(
action: str | None = None,
reason: str | None = None,
rule_label: str | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Disposition instance."""
self.action = action
Expand Down Expand Up @@ -310,7 +310,7 @@ def __init__(
status: str | None = None,
last_visited_on: str | None = None,
has_redirect: bool | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize an EmailDomainVisit instance."""
self.status = status
Expand Down Expand Up @@ -354,7 +354,7 @@ def __init__(
risk: float | None = None,
volume: float | None = None,
visit: dict[str, Any] | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize an EmailDomain instance."""
self.first_seen = first_seen
Expand Down Expand Up @@ -498,7 +498,7 @@ def __init__(
longitude: float | None = None,
distance_to_ip_location: int | None = None,
is_in_ip_country: bool | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a BillingAddress instance."""
self.is_postal_in_city = is_postal_in_city
Expand Down Expand Up @@ -553,7 +553,7 @@ def __init__(
is_in_ip_country: bool | None = None,
is_high_risk: bool | None = None,
distance_to_billing_address: int | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a ShippingAddress instance."""
self.is_postal_in_city = is_postal_in_city
Expand Down Expand Up @@ -604,7 +604,7 @@ def __init__(
matches_postal: bool | None = None,
network_operator: str | None = None,
number_type: str | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Phone instance."""
self.country = country
Expand Down Expand Up @@ -639,7 +639,7 @@ def __init__(
code: str | None = None,
warning: str | None = None,
input_pointer: str | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a ServiceWarning instance."""
self.code = code
Expand Down Expand Up @@ -783,7 +783,7 @@ def __init__(
shipping_address: float | None = None,
shipping_address_distance_to_ip_location: float | None = None,
time_of_day: float | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Subscores instance."""
self.avs_result = avs_result
Expand Down Expand Up @@ -897,7 +897,7 @@ def __init__(
*,
code: str | None = None,
reason: str | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Reason instance."""
self.code = code
Expand All @@ -922,7 +922,7 @@ def __init__(
*,
multiplier: float,
reasons: list[dict[str, Any]] | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a RiskScoreReason instance."""
self.multiplier = multiplier
Expand Down Expand Up @@ -1030,7 +1030,7 @@ def __init__(
subscores: dict[str, Any] | None = None,
warnings: list[dict[str, Any]] | None = None,
risk_score_reasons: list[dict[str, Any]] | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Factors instance."""
self.billing_address = BillingAddress(**(billing_address or {}))
Expand Down Expand Up @@ -1136,7 +1136,7 @@ def __init__(
shipping_address: dict[str, Any] | None = None,
shipping_phone: dict[str, Any] | None = None,
warnings: list[dict[str, Any]] | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize an Insights instance."""
self.billing_address = BillingAddress(**(billing_address or {}))
Expand Down Expand Up @@ -1201,7 +1201,7 @@ def __init__(
queries_remaining: int,
risk_score: float,
warnings: list[dict[str, Any]] | None = None,
**_: Any,
**_: object,
) -> None:
"""Initialize a Score instance."""
self.disposition = Disposition(**(disposition or {}))
Expand Down
4 changes: 2 additions & 2 deletions src/minfraud/webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def __init__( # noqa: PLR0913
:type timeout: float
:param proxy: The URL of an HTTP proxy to use. It may optionally include
a basic auth username and password, e.g.,
``http://username:password@host:port``.
``http://<username>:<password>@<host>:<port>``.
:return: Client object
:rtype: Client
"""
Expand Down Expand Up @@ -510,7 +510,7 @@ def __init__( # noqa: PLR0913
60.
:param proxy: The URL of an HTTP proxy to use. It may optionally include
a basic auth username and password, e.g.,
``http://username:password@host:port``.
``http://<username>:<password>@<host>:<port>``.
:type timeout: float
:return: Client object
:rtype: Client
Expand Down
7 changes: 6 additions & 1 deletion tests/data/factors-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
"is_residential_proxy": true,
"is_tor_exit_node": true,
"network_last_seen": "2025-01-15",
"provider_name": "TestVPN"
"provider_name": "TestVPN",
"residential": {
"confidence": 82,
"network_last_seen": "2026-05-11",
"provider_name": "quickshift"
}
},
"city": {
"confidence": 42,
Expand Down
7 changes: 6 additions & 1 deletion tests/data/insights-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
"is_residential_proxy": true,
"is_tor_exit_node": true,
"network_last_seen": "2025-01-15",
"provider_name": "TestVPN"
"provider_name": "TestVPN",
"residential": {
"confidence": 82,
"network_last_seen": "2026-05-11",
"provider_name": "quickshift"
}
},
"city": {
"confidence": 42,
Expand Down
Loading
Loading