Skip to content

Commit 28cb679

Browse files
committed
Improve type hints
1 parent d850acc commit 28cb679

File tree

10 files changed

+186
-117
lines changed

10 files changed

+186
-117
lines changed

minfraud/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def __init__(
382382
is_prepaid: Optional[bool] = None,
383383
is_virtual: Optional[bool] = None,
384384
# pylint:disable=redefined-builtin
385-
type: Optional[str] = None,
385+
type: Optional[str] = None, # noqa: A002
386386
) -> None:
387387
"""Initialize a CreditCard instance."""
388388
self.issuer = Issuer(**(issuer or {}))

minfraud/request.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@
264264
}
265265

266266

267-
def prepare_report(request: dict[str, Any], validate: bool):
267+
def prepare_report(request: dict[str, Any], validate: bool) -> dict[str, Any]:
268268
"""Validate and prepare minFraud report."""
269269
cleaned_request = _copy_and_clean(request)
270270
if validate:
@@ -279,7 +279,7 @@ def prepare_transaction(
279279
request: dict[str, Any],
280280
validate: bool,
281281
hash_email: bool,
282-
):
282+
) -> dict[str, Any]:
283283
"""Validate and prepare minFraud transaction."""
284284
cleaned_request = _copy_and_clean(request)
285285
if validate:
@@ -306,7 +306,7 @@ def _copy_and_clean(data: Any) -> Any:
306306
return data
307307

308308

309-
def clean_credit_card(credit_card) -> None:
309+
def clean_credit_card(credit_card: dict[str, Any]) -> None:
310310
"""Clean the credit_card input of a transaction request."""
311311
last4 = credit_card.pop("last_4_digits", None)
312312
if last4:
@@ -318,7 +318,7 @@ def clean_credit_card(credit_card) -> None:
318318
credit_card["last_digits"] = last4
319319

320320

321-
def maybe_hash_email(transaction) -> None:
321+
def maybe_hash_email(transaction: dict[str, Any]) -> None:
322322
"""Hash email address in transaction, if present."""
323323
try:
324324
email = transaction["email"]

minfraud/validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from decimal import Decimal
1515
from typing import Optional
1616

17-
from email_validator import validate_email # type: ignore
17+
from email_validator import validate_email
1818
from voluptuous import (
1919
All,
2020
Any,

minfraud/webservice.py

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""Client for minFraud Score, Insights, and Factors."""
22

3+
from __future__ import annotations
4+
35
import json
4-
from collections.abc import Sequence
56
from functools import partial
6-
from typing import Any, Callable, Optional, Union, cast
7+
from typing import TYPE_CHECKING, Any, Callable, cast
78

89
import aiohttp
910
import aiohttp.http
1011
import requests
1112
import requests.utils
12-
from requests.models import Response
1313

1414
from .errors import (
1515
AuthenticationError,
@@ -23,6 +23,18 @@
2323
from .request import prepare_report, prepare_transaction
2424
from .version import __version__
2525

26+
if TYPE_CHECKING:
27+
import sys
28+
import types
29+
from collections.abc import Sequence
30+
31+
from requests.models import Response
32+
33+
if sys.version_info >= (3, 11):
34+
from typing import Self
35+
else:
36+
from typing_extensions import Self
37+
2638
_AIOHTTP_UA = f"minFraud-API/{__version__} {aiohttp.http.SERVER_SOFTWARE}"
2739

2840
_REQUEST_UA = f"minFraud-API/{__version__} {requests.utils.default_user_agent()}"
@@ -68,32 +80,35 @@ def _handle_success(
6880
raw_body: str,
6981
uri: str,
7082
model_class: Callable,
71-
) -> Union[Score, Factors, Insights]:
83+
) -> Score | Factors | Insights:
7284
"""Handle successful response."""
7385
try:
7486
decoded_body = json.loads(raw_body)
7587
except ValueError as ex:
76-
raise MinFraudError(
88+
msg = (
7789
"Received a 200 response but could not decode the "
78-
f"response as JSON: {raw_body}",
90+
f"response as JSON: {raw_body}"
91+
)
92+
raise MinFraudError(
93+
msg,
7994
200,
8095
uri,
8196
) from ex
82-
return model_class(**decoded_body) # type: ignore
97+
return model_class(**decoded_body)
8398

8499
def _exception_for_error(
85100
self,
86101
status: int,
87-
content_type: Optional[str],
102+
content_type: str | None,
88103
raw_body: str,
89104
uri: str,
90-
) -> Union[
91-
AuthenticationError,
92-
InsufficientFundsError,
93-
InvalidRequestError,
94-
HTTPError,
95-
PermissionRequiredError,
96-
]:
105+
) -> (
106+
AuthenticationError
107+
| InsufficientFundsError
108+
| InvalidRequestError
109+
| HTTPError
110+
| PermissionRequiredError
111+
):
97112
"""Return the exception for the error responses."""
98113
if 400 <= status < 500:
99114
return self._exception_for_4xx_status(status, content_type, raw_body, uri)
@@ -104,16 +119,16 @@ def _exception_for_error(
104119
def _exception_for_4xx_status(
105120
self,
106121
status: int,
107-
content_type: Optional[str],
122+
content_type: str | None,
108123
raw_body: str,
109124
uri: str,
110-
) -> Union[
111-
AuthenticationError,
112-
InsufficientFundsError,
113-
InvalidRequestError,
114-
HTTPError,
115-
PermissionRequiredError,
116-
]:
125+
) -> (
126+
AuthenticationError
127+
| InsufficientFundsError
128+
| InvalidRequestError
129+
| HTTPError
130+
| PermissionRequiredError
131+
):
117132
"""Return exception for error responses with 4xx status codes."""
118133
if not raw_body:
119134
return HTTPError(
@@ -161,12 +176,12 @@ def _exception_for_web_service_error(
161176
code: str,
162177
status: int,
163178
uri: str,
164-
) -> Union[
165-
InvalidRequestError,
166-
AuthenticationError,
167-
PermissionRequiredError,
168-
InsufficientFundsError,
169-
]:
179+
) -> (
180+
InvalidRequestError
181+
| AuthenticationError
182+
| PermissionRequiredError
183+
| InsufficientFundsError
184+
):
170185
"""Return exception for error responses with the JSON body."""
171186
if code in (
172187
"ACCOUNT_ID_REQUIRED",
@@ -185,7 +200,7 @@ def _exception_for_web_service_error(
185200
@staticmethod
186201
def _exception_for_5xx_status(
187202
status: int,
188-
raw_body: Optional[str],
203+
raw_body: str | None,
189204
uri: str,
190205
) -> HTTPError:
191206
"""Return exception for error response with 5xx status codes."""
@@ -199,7 +214,7 @@ def _exception_for_5xx_status(
199214
@staticmethod
200215
def _exception_for_unexpected_status(
201216
status: int,
202-
raw_body: Optional[str],
217+
raw_body: str | None,
203218
uri: str,
204219
) -> HTTPError:
205220
"""Return exception for responses with unexpected status codes."""
@@ -215,7 +230,7 @@ class AsyncClient(BaseClient):
215230
"""Async client for accessing the minFraud web services."""
216231

217232
_existing_session: aiohttp.ClientSession
218-
_proxy: Optional[str]
233+
_proxy: str | None
219234

220235
def __init__(
221236
self,
@@ -224,7 +239,7 @@ def __init__(
224239
host: str = "minfraud.maxmind.com",
225240
locales: Sequence[str] = ("en",),
226241
timeout: float = 60,
227-
proxy: Optional[str] = None,
242+
proxy: str | None = None,
228243
) -> None:
229244
"""Construct AsyncClient.
230245
@@ -368,7 +383,7 @@ async def score(
368383

369384
async def report(
370385
self,
371-
report: dict[str, Optional[str]],
386+
report: dict[str, str | None],
372387
validate: bool = True,
373388
) -> None:
374389
"""Send a transaction report to the Report Transaction endpoint.
@@ -405,7 +420,7 @@ async def _response_for(
405420
request: dict[str, Any],
406421
validate: bool,
407422
hash_email: bool,
408-
) -> Union[Score, Factors, Insights]:
423+
) -> Score | Factors | Insights:
409424
"""Send request and create response object."""
410425
prepared_request = prepare_transaction(request, validate, hash_email)
411426
async with await self._do_request(uri, prepared_request) as response:
@@ -443,17 +458,22 @@ async def close(self) -> None:
443458
if hasattr(self, "_existing_session"):
444459
await self._existing_session.close()
445460

446-
async def __aenter__(self) -> "AsyncClient":
461+
async def __aenter__(self) -> Self:
447462
return self
448463

449-
async def __aexit__(self, exc_type: None, exc_value: None, traceback: None) -> None:
464+
async def __aexit__(
465+
self,
466+
exc_type: type[BaseException] | None,
467+
exc_value: BaseException | None,
468+
traceback: types.TracebackType | None,
469+
) -> None:
450470
await self.close()
451471

452472

453473
class Client(BaseClient):
454474
"""Synchronous client for accessing the minFraud web services."""
455475

456-
_proxies: Optional[dict[str, str]]
476+
_proxies: dict[str, str] | None
457477
_session: requests.Session
458478

459479
def __init__(
@@ -463,7 +483,7 @@ def __init__(
463483
host: str = "minfraud.maxmind.com",
464484
locales: Sequence[str] = ("en",),
465485
timeout: float = 60,
466-
proxy: Optional[str] = None,
486+
proxy: str | None = None,
467487
) -> None:
468488
"""Construct Client.
469489
@@ -619,7 +639,7 @@ def score(
619639
),
620640
)
621641

622-
def report(self, report: dict[str, Optional[str]], validate: bool = True) -> None:
642+
def report(self, report: dict[str, str | None], validate: bool = True) -> None:
623643
"""Send a transaction report to the Report Transaction endpoint.
624644
625645
:param report: A dictionary containing the transaction report to be sent
@@ -654,7 +674,7 @@ def _response_for(
654674
request: dict[str, Any],
655675
validate: bool,
656676
hash_email: bool,
657-
) -> Union[Score, Factors, Insights]:
677+
) -> Score | Factors | Insights:
658678
"""Send request and create response object."""
659679
prepared_request = prepare_transaction(request, validate, hash_email)
660680

@@ -681,8 +701,13 @@ def close(self) -> None:
681701
"""
682702
self._session.close()
683703

684-
def __enter__(self) -> "Client":
704+
def __enter__(self) -> Self:
685705
return self
686706

687-
def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None:
707+
def __exit__(
708+
self,
709+
exc_type: type[BaseException] | None,
710+
exc_value: BaseException | None,
711+
traceback: types.TracebackType | None,
712+
) -> None:
688713
self.close()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies = [
1010
"email_validator>=2.0.0,<3.0.0",
1111
"geoip2>=5.0.1,<6.0.0",
1212
"requests>=2.24.0,<3.0.0",
13+
"typing-extensions>=4.13.2",
1314
"voluptuous",
1415
]
1516
requires-python = ">=3.9"

tests/test_models.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1-
import unittest
2-
from typing import Any, Union
1+
from __future__ import annotations
32

4-
from minfraud.models import *
3+
import unittest
4+
from typing import Any
5+
6+
from minfraud.models import (
7+
BillingAddress,
8+
CreditCard,
9+
Device,
10+
Disposition,
11+
Email,
12+
EmailDomain,
13+
Factors,
14+
GeoIP2Location,
15+
Insights,
16+
IPAddress,
17+
Issuer,
18+
Phone,
19+
Reason,
20+
RiskScoreReason,
21+
Score,
22+
ScoreIPAddress,
23+
ServiceWarning,
24+
ShippingAddress,
25+
)
526

627

728
class TestModels(unittest.TestCase):
@@ -23,7 +44,7 @@ def test_shipping_address(self) -> None:
2344
self.assertEqual(200, address.distance_to_billing_address)
2445

2546
@property
26-
def address_dict(self) -> dict[str, Union[bool, float]]:
47+
def address_dict(self) -> dict[str, bool | float]:
2748
return {
2849
"is_in_ip_country": True,
2950
"latitude": 43.1,
@@ -32,7 +53,7 @@ def address_dict(self) -> dict[str, Union[bool, float]]:
3253
"is_postal_in_city": True,
3354
}
3455

35-
def check_address(self, address) -> None:
56+
def check_address(self, address: BillingAddress | ShippingAddress) -> None:
3657
self.assertEqual(True, address.is_in_ip_country)
3758
self.assertEqual(True, address.is_postal_in_city)
3859
self.assertEqual(100, address.distance_to_ip_location)
@@ -374,7 +395,7 @@ def factors_response(self) -> dict[str, Any]:
374395
],
375396
}
376397

377-
def check_insights_data(self, insights, uuid) -> None:
398+
def check_insights_data(self, insights: Insights | Factors, uuid: str) -> None:
378399
self.assertEqual("US", insights.ip_address.country.iso_code)
379400
self.assertEqual(False, insights.ip_address.country.is_in_european_union)
380401
self.assertEqual(True, insights.credit_card.is_business)
@@ -396,7 +417,7 @@ def check_insights_data(self, insights, uuid) -> None:
396417
self.assertEqual("INVALID_INPUT", insights.warnings[0].code)
397418
self.assertIsInstance(insights.warnings, list, "warnings is a list")
398419

399-
def check_risk_score_reasons_data(self, reasons) -> None:
420+
def check_risk_score_reasons_data(self, reasons: list[RiskScoreReason]) -> None:
400421
self.assertEqual(1, len(reasons))
401422
self.assertEqual(45, reasons[0].multiplier)
402423
self.assertEqual(1, len(reasons[0].reasons))

0 commit comments

Comments
 (0)