Skip to content

Commit 0664c51

Browse files
authored
Merge pull request #255 from maxmind/greg/eng-3427
Upgrade Python requirements and expand type coverage
2 parents 037101f + 79a6adf commit 0664c51

File tree

8 files changed

+74
-68
lines changed

8 files changed

+74
-68
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
env: [3.9, "3.10", 3.11, 3.12, 3.13]
18+
env: ["3.10", 3.11, 3.12, 3.13, 3.14]
1919
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
2020
steps:
2121
- uses: actions/checkout@v5

HISTORY.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ History
66
3.2.0
77
++++++++++++++++++
88

9+
* IMPORTANT: Python 3.10 or greater is required. If you are using an older
10+
version, please use an earlier release.
911
* Setuptools has been replaced with the uv build backend for building the
1012
package.
1113
* Added ``securepay`` to the ``/payment/processor`` validation.

pyproject.toml

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ dependencies = [
1313
"typing-extensions>=4.13.2",
1414
"voluptuous",
1515
]
16-
requires-python = ">=3.9"
16+
requires-python = ">=3.10"
1717
readme = "README.rst"
1818
license = "Apache-2.0"
1919
license-files = ["LICENSE"]
@@ -23,11 +23,11 @@ classifiers = [
2323
"Intended Audience :: Developers",
2424
"Programming Language :: Python",
2525
"Programming Language :: Python :: 3",
26-
"Programming Language :: Python :: 3.9",
2726
"Programming Language :: Python :: 3.10",
2827
"Programming Language :: Python :: 3.11",
2928
"Programming Language :: Python :: 3.12",
3029
"Programming Language :: Python :: 3.13",
30+
"Programming Language :: Python :: 3.14",
3131
"Topic :: Internet",
3232
"Topic :: Internet :: Proxy Servers",
3333
"Topic :: Internet :: WWW/HTTP",
@@ -67,9 +67,6 @@ source-include = [
6767
[tool.ruff.lint]
6868
select = ["ALL"]
6969
ignore = [
70-
# Skip type annotation on **_
71-
"ANN003",
72-
7370
# Redundant as the formatter handles missing trailing commas.
7471
"COM812",
7572

@@ -95,16 +92,17 @@ ignorelist = ["id"]
9592

9693
[tool.ruff.lint.per-file-ignores]
9794
"docs/*" = ["ALL"]
98-
"src/minfraud/models.py" = [ "PLR0913" ]
95+
"src/minfraud/models.py" = ["ANN401", "PLR0913"]
96+
"src/minfraud/webservice.py" = ["ANN401"]
9997
"tests/*" = ["ANN201", "D"]
10098

10199
[tool.tox]
102100
env_list = [
103-
"3.9",
104101
"3.10",
105102
"3.11",
106103
"3.12",
107104
"3.13",
105+
"3.14",
108106
"lint",
109107
]
110108
skip_missing_interpreters = false
@@ -119,7 +117,7 @@ commands = [
119117

120118
[tool.tox.env.lint]
121119
description = "Code linting"
122-
python = "3.13"
120+
python = "3.14"
123121
dependency_groups = [
124122
"dev",
125123
"lint",
@@ -131,8 +129,8 @@ commands = [
131129
]
132130

133131
[tool.tox.gh.python]
134-
"3.13" = ["3.13", "lint"]
132+
"3.14" = ["3.14", "lint"]
133+
"3.13" = ["3.13"]
135134
"3.12" = ["3.12"]
136135
"3.11" = ["3.11"]
137136
"3.10" = ["3.10"]
138-
"3.9" = ["3.9"]

src/minfraud/models.py

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import json
6-
from typing import TYPE_CHECKING
6+
from typing import TYPE_CHECKING, Any
77

88
import geoip2.models
99
import geoip2.records
@@ -23,7 +23,7 @@ def __hash__(self) -> int:
2323
# This is not particularly efficient, but I don't expect it to be used much.
2424
return hash(json.dumps(self.to_dict(), sort_keys=True))
2525

26-
def to_dict(self) -> dict: # noqa: C901
26+
def to_dict(self) -> dict[str, Any]: # noqa: C901
2727
"""Return a dict of the object suitable for serialization."""
2828
result = {}
2929
for key, value in self.__dict__.items():
@@ -100,7 +100,7 @@ class GeoIP2Location(geoip2.records.Location):
100100
`RFC 3339 <https://tools.ietf.org/html/rfc3339>`_. For instance, the
101101
local time in Boston might be returned as 2015-04-27T19:17:24-04:00."""
102102

103-
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002
103+
def __init__(self, *args: Any, **kwargs: Any) -> None:
104104
"""Initialize a GeoIP2Location instance."""
105105
self.local_time = kwargs.get("local_time")
106106
super().__init__(*args, **kwargs)
@@ -131,11 +131,11 @@ def __init__(
131131
self,
132132
locales: Sequence[str] | None,
133133
*,
134-
country: dict | None = None,
135-
location: dict | None = None,
134+
country: dict[str, Any] | None = None,
135+
location: dict[str, Any] | None = None,
136136
risk: float | None = None,
137-
risk_reasons: list[dict] | None = None,
138-
**kwargs,
137+
risk_reasons: list[dict[str, Any]] | None = None,
138+
**kwargs: Any,
139139
) -> None:
140140
"""Initialize an IPAddress instance."""
141141
# For raw attribute
@@ -161,7 +161,7 @@ class ScoreIPAddress(_Serializable):
161161
"""This field contains the risk associated with the IP address. The value
162162
ranges from 0.01 to 99. A higher score indicates a higher risk."""
163163

164-
def __init__(self, *, risk: float | None = None, **_) -> None:
164+
def __init__(self, *, risk: float | None = None, **_: Any) -> None:
165165
"""Initialize a ScoreIPAddress instance."""
166166
self.risk = risk
167167

@@ -197,7 +197,7 @@ def __init__(
197197
matches_provided_name: bool | None = None,
198198
phone_number: str | None = None,
199199
matches_provided_phone_number: bool | None = None,
200-
**_,
200+
**_: Any,
201201
) -> None:
202202
"""Initialize an Issuer instance."""
203203
self.name = name
@@ -239,7 +239,7 @@ def __init__(
239239
id: str | None = None,
240240
last_seen: str | None = None,
241241
local_time: str | None = None,
242-
**_,
242+
**_: Any,
243243
) -> None:
244244
"""Initialize a Device instance."""
245245
self.confidence = confidence
@@ -277,7 +277,7 @@ def __init__(
277277
action: str | None = None,
278278
reason: str | None = None,
279279
rule_label: str | None = None,
280-
**_,
280+
**_: Any,
281281
) -> None:
282282
"""Initialize a Disposition instance."""
283283
self.action = action
@@ -293,7 +293,7 @@ class EmailDomain(_Serializable):
293293
was first seen by MaxMind. This is expressed using the ISO 8601 date
294294
format."""
295295

296-
def __init__(self, *, first_seen: str | None = None, **_) -> None:
296+
def __init__(self, *, first_seen: str | None = None, **_: Any) -> None:
297297
"""Initialize an EmailDomain instance."""
298298
self.first_seen = first_seen
299299

@@ -325,7 +325,7 @@ class Email(_Serializable):
325325

326326
def __init__(
327327
self,
328-
domain: dict | None = None,
328+
domain: dict[str, Any] | None = None,
329329
first_seen: str | None = None,
330330
is_disposable: bool | None = None, # noqa: FBT001
331331
is_free: bool | None = None, # noqa: FBT001
@@ -378,7 +378,7 @@ class CreditCard(_Serializable):
378378

379379
def __init__(
380380
self,
381-
issuer: dict | None = None,
381+
issuer: dict[str, Any] | None = None,
382382
country: str | None = None,
383383
brand: str | None = None,
384384
is_business: bool | None = None, # noqa: FBT001
@@ -432,7 +432,7 @@ def __init__(
432432
longitude: float | None = None,
433433
distance_to_ip_location: int | None = None,
434434
is_in_ip_country: bool | None = None,
435-
**_,
435+
**_: Any,
436436
) -> None:
437437
"""Initialize a BillingAddress instance."""
438438
self.is_postal_in_city = is_postal_in_city
@@ -487,7 +487,7 @@ def __init__(
487487
is_in_ip_country: bool | None = None,
488488
is_high_risk: bool | None = None,
489489
distance_to_billing_address: int | None = None,
490-
**_,
490+
**_: Any,
491491
) -> None:
492492
"""Initialize a ShippingAddress instance."""
493493
self.is_postal_in_city = is_postal_in_city
@@ -538,7 +538,7 @@ def __init__(
538538
matches_postal: bool | None = None,
539539
network_operator: str | None = None,
540540
number_type: str | None = None,
541-
**_,
541+
**_: Any,
542542
) -> None:
543543
"""Initialize a Phone instance."""
544544
self.country = country
@@ -573,7 +573,7 @@ def __init__(
573573
code: str | None = None,
574574
warning: str | None = None,
575575
input_pointer: str | None = None,
576-
**_,
576+
**_: Any,
577577
) -> None:
578578
"""Initialize a ServiceWarning instance."""
579579
self.code = code
@@ -717,7 +717,7 @@ def __init__(
717717
shipping_address: float | None = None,
718718
shipping_address_distance_to_ip_location: float | None = None,
719719
time_of_day: float | None = None,
720-
**_,
720+
**_: Any,
721721
) -> None:
722722
"""Initialize a Subscores instance."""
723723
self.avs_result = avs_result
@@ -831,7 +831,7 @@ def __init__(
831831
*,
832832
code: str | None = None,
833833
reason: str | None = None,
834-
**_,
834+
**_: Any,
835835
) -> None:
836836
"""Initialize a Reason instance."""
837837
self.code = code
@@ -855,8 +855,8 @@ def __init__(
855855
self,
856856
*,
857857
multiplier: float,
858-
reasons: list | None = None,
859-
**_,
858+
reasons: list[dict[str, Any]] | None = None,
859+
**_: Any,
860860
) -> None:
861861
"""Initialize a RiskScoreReason instance."""
862862
self.multiplier = multiplier
@@ -948,23 +948,23 @@ def __init__(
948948
self,
949949
locales: Sequence[str],
950950
*,
951-
billing_address: dict | None = None,
952-
billing_phone: dict | None = None,
953-
credit_card: dict | None = None,
954-
disposition: dict | None = None,
951+
billing_address: dict[str, Any] | None = None,
952+
billing_phone: dict[str, Any] | None = None,
953+
credit_card: dict[str, Any] | None = None,
954+
disposition: dict[str, Any] | None = None,
955955
funds_remaining: float,
956-
device: dict | None = None,
957-
email: dict | None = None,
956+
device: dict[str, Any] | None = None,
957+
email: dict[str, Any] | None = None,
958958
id: str,
959-
ip_address: dict | None = None,
959+
ip_address: dict[str, Any] | None = None,
960960
queries_remaining: int,
961961
risk_score: float,
962-
shipping_address: dict | None = None,
963-
shipping_phone: dict | None = None,
964-
subscores: dict | None = None,
965-
warnings: list[dict] | None = None,
966-
risk_score_reasons: list[dict] | None = None,
967-
**_,
962+
shipping_address: dict[str, Any] | None = None,
963+
shipping_phone: dict[str, Any] | None = None,
964+
subscores: dict[str, Any] | None = None,
965+
warnings: list[dict[str, Any]] | None = None,
966+
risk_score_reasons: list[dict[str, Any]] | None = None,
967+
**_: Any,
968968
) -> None:
969969
"""Initialize a Factors instance."""
970970
self.billing_address = BillingAddress(**(billing_address or {}))
@@ -1056,21 +1056,21 @@ def __init__(
10561056
self,
10571057
locales: Sequence[str],
10581058
*,
1059-
billing_address: dict | None = None,
1060-
billing_phone: dict | None = None,
1061-
credit_card: dict | None = None,
1062-
device: dict | None = None,
1063-
disposition: dict | None = None,
1064-
email: dict | None = None,
1059+
billing_address: dict[str, Any] | None = None,
1060+
billing_phone: dict[str, Any] | None = None,
1061+
credit_card: dict[str, Any] | None = None,
1062+
device: dict[str, Any] | None = None,
1063+
disposition: dict[str, Any] | None = None,
1064+
email: dict[str, Any] | None = None,
10651065
funds_remaining: float,
10661066
id: str,
1067-
ip_address: dict | None = None,
1067+
ip_address: dict[str, Any] | None = None,
10681068
queries_remaining: int,
10691069
risk_score: float,
1070-
shipping_address: dict | None = None,
1071-
shipping_phone: dict | None = None,
1072-
warnings: list[dict] | None = None,
1073-
**_,
1070+
shipping_address: dict[str, Any] | None = None,
1071+
shipping_phone: dict[str, Any] | None = None,
1072+
warnings: list[dict[str, Any]] | None = None,
1073+
**_: Any,
10741074
) -> None:
10751075
"""Initialize an Insights instance."""
10761076
self.billing_address = BillingAddress(**(billing_address or {}))
@@ -1128,14 +1128,14 @@ class Score(_Serializable):
11281128
def __init__(
11291129
self,
11301130
*,
1131-
disposition: dict | None = None,
1131+
disposition: dict[str, Any] | None = None,
11321132
funds_remaining: float,
11331133
id: str,
1134-
ip_address: dict | None = None,
1134+
ip_address: dict[str, Any] | None = None,
11351135
queries_remaining: int,
11361136
risk_score: float,
1137-
warnings: list[dict] | None = None,
1138-
**_,
1137+
warnings: list[dict[str, Any]] | None = None,
1138+
**_: Any,
11391139
) -> None:
11401140
"""Initialize a Score instance."""
11411141
self.disposition = Disposition(**(disposition or {}))

src/minfraud/validation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import urllib.parse
1515
import uuid
1616
from decimal import Decimal
17+
from typing import Any as AnyType
1718

1819
from email_validator import validate_email
1920
from voluptuous import (
@@ -327,7 +328,7 @@ def _uri(s: str) -> str:
327328
return s
328329

329330

330-
validate_transaction = Schema(
331+
validate_transaction: Schema = Schema(
331332
{
332333
"account": {
333334
"user_id": str,
@@ -459,7 +460,7 @@ def _validate_at_least_one_identifier_field(report: dict) -> bool:
459460
return True
460461

461462

462-
def validate_report(report: dict) -> bool:
463+
def validate_report(report: dict[str, AnyType]) -> bool:
463464
"""Validate minFraud Transaction Report fields."""
464465
_validate_report_schema(report)
465466
_validate_at_least_one_identifier_field(report)

0 commit comments

Comments
 (0)