Skip to content

Commit 1520d0b

Browse files
committed
refactor: modernize routing, security, and linting
- feat(routing): implement O(1) registry-based routing with safe URI interpolation - feat(security): mitigate CWE-22 (Path Traversal) via centralized segment sanitization - feat(builders): add TemplateContentBuilder for structured Content API payloads - fix(linting): resolve all docstring and import grouping lint errors - refactor: centralize types and constants in types.py to reduce boot-time overhead - test: expand test suite with parameterization for registry parity and security
1 parent 3890005 commit 1520d0b

11 files changed

Lines changed: 360 additions & 73 deletions

File tree

mailjet_rest/__init__.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
"""Mailjet REST API Python Wrapper."""
22

3-
from mailjet_rest.client import Client
4-
from mailjet_rest.client import Config
5-
from mailjet_rest.errors import ActionDeniedError
6-
from mailjet_rest.errors import ApiError
7-
from mailjet_rest.errors import ApiRateLimitError
8-
from mailjet_rest.errors import AuthorizationError
9-
from mailjet_rest.errors import CriticalApiError
10-
from mailjet_rest.errors import DoesNotExistError
11-
from mailjet_rest.errors import MailjetApiError
12-
from mailjet_rest.errors import MailjetAuthError
13-
from mailjet_rest.errors import MailjetNetworkError
14-
from mailjet_rest.errors import TimeoutError # noqa: A004
15-
from mailjet_rest.errors import ValidationError
3+
from mailjet_rest.client import Client, Config
4+
from mailjet_rest.errors import (
5+
ActionDeniedError,
6+
ApiError,
7+
ApiRateLimitError,
8+
AuthorizationError,
9+
CriticalApiError,
10+
DoesNotExistError,
11+
MailjetApiError,
12+
MailjetAuthError,
13+
MailjetNetworkError,
14+
TimeoutError, # noqa: A004
15+
ValidationError,
16+
)
1617
from mailjet_rest.utils.version import get_version
1718

1819

mailjet_rest/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.6.0.post1.dev20"
1+
__version__ = "1.6.0.post1.dev22"

mailjet_rest/builders.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import json
77
import mimetypes
88
import sys
9-
from typing import TYPE_CHECKING
10-
from typing import Any
9+
from typing import TYPE_CHECKING, Any
1110

1211

1312
if sys.version_info >= (3, 11):

mailjet_rest/client.py

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,34 @@
1212
import sys
1313
import warnings
1414
from contextlib import suppress
15-
from typing import TYPE_CHECKING
16-
from typing import Any
17-
from typing import ClassVar
18-
from typing import cast
15+
from typing import TYPE_CHECKING, Any, ClassVar, cast
1916

2017
import requests # pyright: ignore[reportMissingModuleSource]
21-
from requests.exceptions import ConnectionError as RequestsConnectionError
22-
from requests.exceptions import RequestException
23-
from requests.exceptions import Timeout as RequestsTimeout
18+
from requests.exceptions import ConnectionError as RequestsConnectionError, RequestException, Timeout as RequestsTimeout
2419
from urllib3.util.retry import Retry
2520

2621
from mailjet_rest.config import Config
2722
from mailjet_rest.endpoint import Endpoint
28-
from mailjet_rest.errors import ActionDeniedError
29-
from mailjet_rest.errors import ApiError
30-
from mailjet_rest.errors import ApiRateLimitError
31-
from mailjet_rest.errors import AuthorizationError
32-
from mailjet_rest.errors import CriticalApiError
33-
from mailjet_rest.errors import DoesNotExistError
34-
from mailjet_rest.errors import MailjetAuthError
35-
from mailjet_rest.errors import TimeoutError # noqa: A004
36-
from mailjet_rest.errors import ValidationError
23+
from mailjet_rest.errors import (
24+
ActionDeniedError,
25+
ApiError,
26+
ApiRateLimitError,
27+
AuthorizationError,
28+
CriticalApiError,
29+
DoesNotExistError,
30+
MailjetAuthError,
31+
TimeoutError, # noqa: A004
32+
ValidationError,
33+
)
3734
from mailjet_rest.routes import ROUTE_MAP
3835
from mailjet_rest.types import _ALLOWED_TRACE_FIELDS
39-
from mailjet_rest.utils.guardrails import RedactingFilter
40-
from mailjet_rest.utils.guardrails import SecureHTTPAdapter
41-
from mailjet_rest.utils.guardrails import SecurityGuard
36+
from mailjet_rest.utils.guardrails import RedactingFilter, SecureHTTPAdapter, SecurityGuard
4237

4338

4439
if TYPE_CHECKING:
4540
from types import TracebackType
4641

47-
from mailjet_rest.types import HttpMethod
48-
from mailjet_rest.types import PayloadType
49-
from mailjet_rest.types import TimeoutType
42+
from mailjet_rest.types import HttpMethod, PayloadType, TimeoutType
5043

5144

5245
if sys.version_info >= (3, 11):

mailjet_rest/config.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
"""Configuration settings for the Mailjet SDK."""
22

33
from dataclasses import dataclass
4-
from typing import ClassVar
5-
from typing import cast
4+
from typing import ClassVar, cast
65

76
from mailjet_rest._version import __version__
8-
from mailjet_rest.types import _DEFAULT_TIMEOUT
9-
from mailjet_rest.types import _JSON_HEADERS
10-
from mailjet_rest.types import _TEXT_HEADERS
11-
from mailjet_rest.types import TimeoutType
7+
from mailjet_rest.types import _DEFAULT_TIMEOUT, _JSON_HEADERS, _TEXT_HEADERS, TimeoutType
128
from mailjet_rest.utils.guardrails import SecurityGuard
139

1410

mailjet_rest/endpoint.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,18 @@
44

55
import re
66
import warnings
7-
from dataclasses import dataclass
8-
from dataclasses import field
9-
from typing import TYPE_CHECKING
10-
from typing import Any
7+
from dataclasses import dataclass, field
8+
from typing import TYPE_CHECKING, Any
119
from urllib.parse import quote
1210

1311
from mailjet_rest.routes import ROUTE_MAP
14-
from mailjet_rest.types import _JSON_HEADERS
15-
from mailjet_rest.types import _TEXT_HEADERS
16-
from mailjet_rest.types import HttpMethod
17-
from mailjet_rest.types import PayloadType
18-
from mailjet_rest.types import TimeoutType
12+
from mailjet_rest.types import _JSON_HEADERS, _TEXT_HEADERS, HttpMethod, PayloadType, TimeoutType
1913
from mailjet_rest.utils.guardrails import SecurityGuard
2014

2115

2216
# Prevent circular import at runtime
2317
if TYPE_CHECKING:
24-
from collections.abc import Callable
25-
from collections.abc import Generator
18+
from collections.abc import Callable, Generator
2619

2720
import requests
2821

mailjet_rest/routes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from __future__ import annotations
44

55
from types import MappingProxyType
6-
from typing import Final
7-
from typing import NamedTuple
6+
from typing import Final, NamedTuple
87

98

109
class Route(NamedTuple):

mailjet_rest/types.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""Type definitions and constants for the Mailjet SDK."""
22

3+
from __future__ import annotations
4+
35
import sys
46
from types import MappingProxyType
5-
from typing import Any
6-
from typing import Final
7-
from typing import Literal
8-
from typing import TypeAlias
9-
from typing import TypedDict
7+
from typing import Any, Final, Literal, TypeAlias, TypedDict
108

119

1210
if sys.version_info >= (3, 11):

mailjet_rest/utils/guardrails.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
import ssl
66
import sys
77
import warnings
8+
from functools import lru_cache
89
from pathlib import Path
9-
from typing import Any
10-
from typing import ClassVar
11-
from typing import Final
12-
from urllib.parse import quote
13-
from urllib.parse import urlparse
10+
from typing import Any, ClassVar, Final
11+
from urllib.parse import quote, urlparse
1412

1513
from requests.adapters import HTTPAdapter
1614

@@ -23,10 +21,18 @@
2321

2422
_CRLF_RE: Final = re.compile(r"[\r\n]")
2523

24+
2625
# Regex to catch Authorization headers and common API key patterns
27-
_SECRET_PATTERN = re.compile(
28-
r"(?i)(Authorization|api[_-]key|api[_-]secret|token)([:\s=]+(?:Bearer\s+|Basic\s+|Token\s+)?)([^\s'\"]+)"
29-
)
26+
@lru_cache(maxsize=1)
27+
def _get_secret_pattern() -> re.Pattern[str]:
28+
"""Lazy-compile strict patterns to minimize cold-boot overhead.
29+
30+
Returns:
31+
re.Pattern[str]: Compiled regular expression for secret pattern matching.
32+
"""
33+
return re.compile(
34+
r"(?i)(Authorization|api[_-]key|api[_-]secret|token)([:\s=]+(?:Bearer\s+|Basic\s+|Token\\s+)?)([^\s'\"]+)"
35+
)
3036

3137

3238
class SecureHTTPAdapter(HTTPAdapter):
@@ -54,14 +60,14 @@ def filter(self, record: logging.LogRecord) -> bool:
5460
bool
5561
"""
5662
if isinstance(record.msg, str):
57-
record.msg = _SECRET_PATTERN.sub(r"\1\2********", record.msg)
63+
record.msg = _get_secret_pattern().sub(r"\1\2********", record.msg)
5864

5965
# Redact arguments
6066
if record.args:
6167
new_args: list[Any] = []
6268
for arg in record.args:
6369
if isinstance(arg, str):
64-
new_args.append(_SECRET_PATTERN.sub(r"\1\2********", arg))
70+
new_args.append(_get_secret_pattern().sub(r"\1\2********", arg))
6571
else:
6672
new_args.append(arg)
6773
record.args = tuple(new_args)

0 commit comments

Comments
 (0)