Skip to content

Commit

Permalink
🔧 Add support for Python 3.13 (687)
Browse files Browse the repository at this point in the history
🔧 Add support for Python 3.13
  • Loading branch information
yezz123 authored Oct 20, 2024
2 parents 86b6b34 + 850c9a3 commit f25cca7
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 215 deletions.
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
fail-fast: false

steps:
Expand Down Expand Up @@ -43,7 +43,7 @@ jobs:

matrix:

python-version: [ "3.9", "3.10", "3.11", "3.12"]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13"]

os: [ubuntu, macos, windows]

Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:

matrix:

python-version: [ "3.9", "3.10", "3.11", "3.12"]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13"]

os: [ubuntu, macos, windows]

Expand Down Expand Up @@ -146,7 +146,7 @@ jobs:

matrix:

python-version: [ "3.9", "3.10", "3.11", "3.12"]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13"]

os: [ubuntu]

Expand All @@ -161,9 +161,6 @@ jobs:
- name: Clone authx extra
run: git clone https://github.com/yezz123/authx-extra.git --single-branch

- name: Update pip
run: python -m pip install --upgrade pip

- name: Test Suite - ${{ matrix.os }} - py${{ matrix.python-version }}
run: bash scripts/test_extra.sh
env:
Expand Down
5 changes: 1 addition & 4 deletions authx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
TokenLocations,
)

PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")


if PYDANTIC_V2:
if PYDANTIC_V2 := PYDANTIC_VERSION.startswith("2."):
from pydantic_settings import BaseSettings # pragma: no cover
else:
from pydantic import BaseSettings # type: ignore # pragma: no cover
Expand Down
9 changes: 5 additions & 4 deletions authx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Dict,
Literal,
Optional,
Union,
overload,
)

Expand Down Expand Up @@ -56,16 +57,16 @@ def __init__(
config (AuthXConfig, optional): Configuration instance to use. Defaults to AuthXConfig().
model (Optional[T], optional): Model type hint. Defaults to Dict[str, Any].
"""
self.model = model if model is not None else {}
self.model: Union[T, Dict[str, Any]] = model if model is not None else {}
super().__init__(model=model)
super(_CallbackHandler, self).__init__()
self._config = config

def load_config(self, config: AuthXConfig) -> None:
"""Loads a AuthXConfig as the new configuration
Args:
config (AuthXConfig): Configuration to load
b
Args:
config (AuthXConfig): Configuration to load
"""
self._config = config

Expand Down
4 changes: 1 addition & 3 deletions authx/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
TokenType,
)

PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")

if PYDANTIC_V2:
if PYDANTIC_V2 := PYDANTIC_VERSION.startswith("2."):
from pydantic import ConfigDict, field_validator # pragma: no cover
else:
from pydantic import Extra, validator # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion authx/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def create_token(
if data:
jwt_claims.update(data)

payload = {**additional_claims, **jwt_claims}
payload = additional_claims | jwt_claims

return jwt.encode(payload=payload, key=key, algorithm=algorithm, headers=headers)

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "authx"
description = "Ready to use and customizable Authentications and Oauth2 management for FastAPI"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Yasser Tahiri", email = "hello@yezz.me" },
Expand All @@ -33,11 +33,11 @@ classifiers = [
"Framework :: Pydantic :: 2",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Internet :: WWW/HTTP :: Session",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand Down
6 changes: 4 additions & 2 deletions scripts/test_extra.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ set -e

pushd "$(dirname $0)/../authx-extra"

pip install -r requirements/pyproject.txt && pip install -r requirements/testing.txt
pip install uv

pip install -e ../
uv sync

source .venv/bin/activate

pytest --cov=authx_extra --cov-report=xml

Expand Down
14 changes: 9 additions & 5 deletions tests/internal/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,19 @@ def test_get_store():
assert store.get_store("nonexistent-id") is None


def populate_old_sessions(memory_io, count, created_at):
for i in range(count):
memory_io.raw_memory_store[str(i)] = {
"created_at": created_at,
"store": {},
}


def test_gc_cleanup_old_sessions(memory_io):
# Populate raw_memory_store with 100 sessions older than 12 hours
current_time = int(time())
twelve_hours_ago = current_time - 3600 * 12
for i in range(100):
memory_io.raw_memory_store[str(i)] = {
"created_at": twelve_hours_ago,
"store": {},
}
populate_old_sessions(memory_io, 100, twelve_hours_ago)

# Add one more session within 12 hours
extra_session_id = "1000"
Expand Down
47 changes: 30 additions & 17 deletions tests/test_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,37 @@ def token_callback(token: str, **kwargs) -> bool:
assert not handler.is_token_in_blocklist(None)


def test_edge_case_kwargs_passing():
handler = _CallbackHandler()
def model_callback(uid: str, **kwargs) -> Optional[DummyModel]:
if "extra" in kwargs and kwargs["extra"] == "special":
return DummyModel(f"special_{uid}")
return DummyModel(uid)

def model_callback(uid: str, **kwargs) -> Optional[DummyModel]:
if "extra" in kwargs and kwargs["extra"] == "special":
return DummyModel(f"special_{uid}")
return DummyModel(uid)

def token_callback(token: str, **kwargs) -> bool:
return "block_all" in kwargs and kwargs["block_all"]
def token_callback(token: str, **kwargs) -> bool:
return "block_all" in kwargs and kwargs["block_all"]


@pytest.fixture
def handler():
handler = _CallbackHandler()
handler.set_callback_get_model_instance(model_callback)
handler.set_callback_token_blocklist(token_callback)
return handler


def test_edge_case_kwargs_passing_special(handler):
assert handler._get_current_subject("123", extra="special").id == "special_123"


def test_edge_case_kwargs_passing_normal(handler):
assert handler._get_current_subject("123").id == "123"


def test_token_in_blocklist(handler):
assert handler.is_token_in_blocklist("any_token", block_all=True)


def test_token_not_in_blocklist(handler):
assert not handler.is_token_in_blocklist("any_token", block_all=False)


Expand Down Expand Up @@ -190,6 +203,15 @@ def test_check_token_callback_is_set():
assert handler._check_token_callback_is_set(ignore_errors=True) == True


def complex_callback(token, **kwargs):
if token == "blocked":
return True
elif token == "none":
return None
else:
return False


def test_is_token_in_blocklist_detailed():
handler = _CallbackHandler()

Expand All @@ -208,15 +230,6 @@ def test_is_token_in_blocklist_detailed():
handler.set_callback_token_blocklist(lambda token, **kwargs: None)
assert not handler.is_token_in_blocklist("token")

# Test with a more complex callback
def complex_callback(token, **kwargs):
if token == "blocked":
return True
elif token == "none":
return None
else:
return False

handler.set_callback_token_blocklist(complex_callback)
assert handler.is_token_in_blocklist("blocked")
assert not handler.is_token_in_blocklist("none")
Expand Down
36 changes: 18 additions & 18 deletions tests/test_implicit_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ async def mock_middleware(request: Request, call_next):
mock_token = Mock()
mock_payload = Mock(time_until_expiry=100, sub="user123", extra_dict={})

with patch.object(
authx, "_get_token_from_request", return_value=mock_token
), patch.object(authx, "verify_token", return_value=mock_payload), patch.object(
authx, "create_access_token", return_value="new_token"
), patch.object(authx, "set_access_cookies"), patch.object(
authx, "_implicit_refresh_enabled_for_request", return_value=True
with (
patch.object(authx, "_get_token_from_request", return_value=mock_token),
patch.object(authx, "verify_token", return_value=mock_payload),
patch.object(authx, "create_access_token", return_value="new_token"),
patch.object(authx, "set_access_cookies"),
patch.object(authx, "_implicit_refresh_enabled_for_request", return_value=True),
):
response = client.get("/test")
assert response.status_code == 200
Expand All @@ -98,11 +98,12 @@ async def call_next(request):
mock_token = Mock()
mock_payload = Mock(time_until_expiry=100, sub="user123", extra_dict={})

with patch.object(
authx, "_get_token_from_request", return_value=mock_token
), patch.object(authx, "verify_token", return_value=mock_payload), patch.object(
authx, "create_access_token", return_value="new_token"
), patch.object(authx, "set_access_cookies"):
with (
patch.object(authx, "_get_token_from_request", return_value=mock_token),
patch.object(authx, "verify_token", return_value=mock_payload),
patch.object(authx, "create_access_token", return_value="new_token"),
patch.object(authx, "set_access_cookies"),
):
response = client.get("/test")
assert response.status_code == 200
assert response.json() == {"message": "success"}
Expand All @@ -124,13 +125,12 @@ async def call_next(request):
mock_token = Mock()
mock_payload = Mock(time_until_expiry=1000, sub="user123", extra_dict={})

with patch.object(
authx, "_get_token_from_request", return_value=mock_token
), patch.object(authx, "verify_token", return_value=mock_payload), patch.object(
authx, "create_access_token"
) as mock_create_token, patch.object(
authx, "set_access_cookies"
) as mock_set_cookies:
with (
patch.object(authx, "_get_token_from_request", return_value=mock_token),
patch.object(authx, "verify_token", return_value=mock_payload),
patch.object(authx, "create_access_token") as mock_create_token,
patch.object(authx, "set_access_cookies") as mock_set_cookies,
):
response = client.get("/test")
assert response.status_code == 200
assert response.json() == {"message": "success"}
Expand Down
41 changes: 31 additions & 10 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ def test_payload_has_scopes_empty(valid_payload: TokenPayload):
assert not valid_payload.has_scopes("read", "write")


def test_payload_extra_dict():
@pytest.mark.skipif(not PYDANTIC_V2, reason="Test for Pydantic V2")
def test_payload_extra_dict_pydantic_v2():
payload = TokenPayload(
type="access",
fresh=True,
Expand All @@ -352,10 +353,25 @@ def test_payload_extra_dict():
).timestamp(),
extra="EXTRA",
)
if PYDANTIC_V2:
assert payload.extra_dict == {}
else:
assert payload.extra_dict == {"extra": "EXTRA"}
assert payload.extra_dict == {}


@pytest.mark.skipif(PYDANTIC_V2, reason="Test for Pydantic V1")
def test_payload_extra_dict_pydantic_v1():
payload = TokenPayload(
type="access",
fresh=True,
sub="BOOOM",
csrf="CSRF_TOKEN",
scopes=["read", "write"],
exp=datetime.timedelta(minutes=20),
nbf=datetime.datetime(2000, 1, 1, 12, 0, tzinfo=datetime.timezone.utc),
iat=datetime.datetime(
2000, 1, 1, 12, 0, tzinfo=datetime.timezone.utc
).timestamp(),
extra="EXTRA",
)
assert payload.extra_dict == {"extra": "EXTRA"}


def test_verify_token_type_exception():
Expand Down Expand Up @@ -394,13 +410,18 @@ def test_token_payload_creation(sample_payload):
assert payload.scopes == ["read", "write"]


def test_token_payload_extra_fields(sample_payload):
@pytest.mark.skipif(not PYDANTIC_V2, reason="Test for Pydantic V2")
def test_token_payload_extra_fields_pydantic_v2(sample_payload):
sample_payload["extra_field"] = "extra_value"
payload = TokenPayload(**sample_payload)
assert payload.extra_dict == {}


@pytest.mark.skipif(PYDANTIC_V2, reason="Test for Pydantic V1")
def test_token_payload_extra_fields_pydantic_v1(sample_payload):
sample_payload["extra_field"] = "extra_value"
payload = TokenPayload(**sample_payload)
if PYDANTIC_V2:
assert payload.extra_dict == {}
else:
assert payload.extra_dict == {"extra_field": "extra_value", "name": "John Doe"}
assert payload.extra_dict == {"extra_field": "extra_value", "name": "John Doe"}


def test_token_payload_encode_decode():
Expand Down
Loading

0 comments on commit f25cca7

Please sign in to comment.