Skip to content

Commit bdd80e8

Browse files
committed
refactor(all): create specific modules for typing
1 parent a0f6632 commit bdd80e8

File tree

15 files changed

+263
-229
lines changed

15 files changed

+263
-229
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FAthroniaeth%2Ffastapi-api-key%2Fmain%2Fpyproject.toml)
44
[![Tested with pytest](https://img.shields.io/badge/tests-pytest-informational.svg)](https://pytest.org/)
5-
![Coverage](https://img.shields.io/badge/coverage-69%25-brightgreen.svg)
5+
![Coverage](https://img.shields.io/badge/coverage-72%25-brightgreen.svg)
66
[![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-4B32C3.svg)](https://docs.astral.sh/ruff/)
77
[![Security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://bandit.readthedocs.io/)
88
[![Deps: uv](https://img.shields.io/badge/deps-managed%20with%20uv-3E4DD8.svg)](https://docs.astral.sh/uv/)

src/fastapi_api_key/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import subprocess # nosec: B404
44

55
from fastapi_api_key.domain.entities import ApiKey
6-
from fastapi_api_key.service import ApiKeyService
6+
from fastapi_api_key.services.service import ApiKeyService
77
from fastapi_api_key.api import create_api_keys_router, create_depends_api_key
88
from fastapi_api_key.cli import create_api_keys_cli
99

src/fastapi_api_key/api.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import warnings
22

33

4-
from fastapi_api_key.service import AbstractApiKeyService
4+
from fastapi_api_key.services.service import AbstractApiKeyService
5+
from fastapi_api_key.types import SecurityHTTPBearer, SecurityAPIKeyHeader
56

67
try:
78
import fastapi # noqa: F401
@@ -96,7 +97,7 @@ class DeletedResponse(BaseModel):
9697
def _to_out(entity: ApiKey) -> ApiKeyOut:
9798
"""Map an `ApiKey` entity to the public `ApiKeyOut` schema."""
9899
return ApiKeyOut(
99-
id=str(entity.id_),
100+
id="entity.id_",
100101
name=entity.name,
101102
description=entity.description,
102103
is_active=entity.is_active,
@@ -351,10 +352,6 @@ async def _handle_verify_key(
351352
) from exc
352353

353354

354-
SecurityHTTPBearer = Union[Callable[[HTTPAuthorizationCredentials], Awaitable[D]]]
355-
SecurityAPIKeyHeader = Union[Callable[[str], Awaitable[D]]]
356-
357-
358355
def create_depends_api_key(
359356
depends_svc_api_keys: Callable[[...], Awaitable[AbstractApiKeyService[D]]],
360357
security: Optional[Union[HTTPBearer, APIKeyHeader]] = None,

src/fastapi_api_key/cli.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import asyncio
22
import json
3-
from contextlib import AbstractAsyncContextManager
43
from dataclasses import asdict, is_dataclass
54
from datetime import datetime, timezone
6-
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional
5+
from typing import Any, Awaitable, Callable, Optional
76

8-
from fastapi_api_key.domain.entities import ApiKeyEntity
7+
from fastapi_api_key.domain.base import ApiKeyEntity
98
from fastapi_api_key.domain.errors import (
109
InvalidKey,
1110
KeyExpired,
1211
KeyInactive,
1312
KeyNotFound,
1413
KeyNotProvided,
1514
)
16-
from fastapi_api_key.service import AbstractApiKeyService
17-
18-
if TYPE_CHECKING:
19-
pass
20-
21-
ServiceFactory = Callable[[], AbstractAsyncContextManager[AbstractApiKeyService[Any]]]
22-
"""Callable returning an async context manager that yields an API key service instance."""
15+
from fastapi_api_key.types import ServiceFactory
2316

2417
DomainErrors = (
2518
InvalidKey,

src/fastapi_api_key/domain/base.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from datetime import datetime
2+
from typing import Optional, runtime_checkable, Protocol, TypeVar
3+
4+
5+
@runtime_checkable
6+
class ApiKeyEntity(Protocol):
7+
"""Protocol for an API key entity.
8+
9+
Attributes:
10+
id_ (str): Unique identifier for the API key.
11+
name (Optional[str]): Optional name for the API key.
12+
description (Optional[str]): Optional description for the API key.
13+
is_active (bool): Indicates if the API key is active.
14+
expires_at (Optional[datetime]): Optional expiration datetime for the API key.
15+
created_at (datetime): Datetime when the API key was created.
16+
last_used_at (Optional[datetime]): Optional datetime when the API key was last used.
17+
key_id (str): Public identifier part of the API key.
18+
key_hash (Optional[str]): Hashed secret part of the API key.
19+
_key_secret (str): The secret part of the API key, only available at creation time.
20+
_key_secret_first (str): First part of the secret for display purposes.
21+
_key_secret_last (str): Last part of the secret for display purposes.
22+
"""
23+
24+
id_: str
25+
name: Optional[str]
26+
description: Optional[str]
27+
is_active: bool
28+
expires_at: Optional[datetime]
29+
created_at: datetime
30+
last_used_at: Optional[datetime]
31+
key_id: str
32+
key_hash: Optional[str]
33+
_key_secret: Optional[str]
34+
_key_secret_first: Optional[str]
35+
_key_secret_last: Optional[str]
36+
37+
@property
38+
def key_secret(self) -> Optional[str]:
39+
"""The secret part of the API key, only available at creation time."""
40+
key_secret = self._key_secret
41+
self._key_secret = None # Clear after first access
42+
return key_secret
43+
44+
@property
45+
def key_secret_first(self) -> str:
46+
"""First part of the secret for display purposes/give the user a clue as to which key we are talking about."""
47+
...
48+
49+
@property
50+
def key_secret_last(self) -> str:
51+
"""Last part of the secret for display purposes/give the user a clue as to which key we are talking about."""
52+
...
53+
54+
def full_key_secret(
55+
self,
56+
global_prefix: str,
57+
separator: str,
58+
key_secret: str,
59+
) -> str:
60+
"""Construct the full API key string to be given to the user."""
61+
...
62+
63+
def disable(self) -> None:
64+
"""Disable the API key so it cannot be used for authentication."""
65+
...
66+
67+
def enable(self) -> None:
68+
"""Enable the API key so it can be used for authentication."""
69+
...
70+
71+
def touch(self) -> None:
72+
"""Mark the key as used now. Trigger for each ensured authentication."""
73+
...
74+
75+
def ensure_can_authenticate(self) -> None:
76+
"""Raise domain errors if this key cannot be used for authentication.
77+
78+
Raises:
79+
ApiKeyDisabledError: If the key is disabled.
80+
ApiKeyExpiredError: If the key is expired.
81+
"""
82+
...
83+
84+
85+
D = TypeVar("D", bound=ApiKeyEntity)
86+
"""Domain entity type variable bound to any ApiKeyEntity subclass."""

src/fastapi_api_key/domain/entities.py

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from dataclasses import field, dataclass
22
from datetime import datetime, timezone
3-
from typing import Optional, runtime_checkable, Protocol, TypeVar
3+
from typing import Optional
44

5+
from fastapi_api_key.domain.base import ApiKeyEntity
56
from fastapi_api_key.domain.errors import KeyExpired, KeyInactive
67
from fastapi_api_key.utils import (
78
uuid_factory,
@@ -10,88 +11,6 @@
1011
)
1112

1213

13-
@runtime_checkable
14-
class ApiKeyEntity(Protocol):
15-
"""Protocol for an API key entity.
16-
17-
Attributes:
18-
id_ (str): Unique identifier for the API key.
19-
name (Optional[str]): Optional name for the API key.
20-
description (Optional[str]): Optional description for the API key.
21-
is_active (bool): Indicates if the API key is active.
22-
expires_at (Optional[datetime]): Optional expiration datetime for the API key.
23-
created_at (datetime): Datetime when the API key was created.
24-
last_used_at (Optional[datetime]): Optional datetime when the API key was last used.
25-
key_id (str): Public identifier part of the API key.
26-
key_hash (Optional[str]): Hashed secret part of the API key.
27-
key_secret_first (str): First part of the secret for display purposes.
28-
key_secret_last (str): Last part of the secret for display purposes.
29-
"""
30-
31-
id_: str
32-
name: Optional[str]
33-
description: Optional[str]
34-
is_active: bool
35-
expires_at: Optional[datetime]
36-
created_at: datetime
37-
last_used_at: Optional[datetime]
38-
key_id: str
39-
key_hash: Optional[str]
40-
_key_secret: Optional[str]
41-
_key_secret_first: Optional[str]
42-
_key_secret_last: Optional[str]
43-
44-
@property
45-
def key_secret(self) -> Optional[str]:
46-
"""The secret part of the API key, only available at creation time."""
47-
key_secret = self._key_secret
48-
self._key_secret = None # Clear after first access
49-
return key_secret
50-
51-
@property
52-
def key_secret_first(self) -> str:
53-
"""First part of the secret for display purposes/give the user a clue as to which key we are talking about."""
54-
...
55-
56-
@property
57-
def key_secret_last(self) -> str:
58-
"""Last part of the secret for display purposes/give the user a clue as to which key we are talking about."""
59-
...
60-
61-
def full_key_secret(
62-
self,
63-
global_prefix: str,
64-
separator: str,
65-
key_secret: str,
66-
) -> str:
67-
"""Construct the full API key string to be given to the user."""
68-
...
69-
70-
def disable(self) -> None:
71-
"""Disable the API key so it cannot be used for authentication."""
72-
...
73-
74-
def enable(self) -> None:
75-
"""Enable the API key so it can be used for authentication."""
76-
...
77-
78-
def touch(self) -> None:
79-
"""Mark the key as used now. Trigger for each ensured authentication."""
80-
...
81-
82-
def ensure_can_authenticate(self) -> None:
83-
"""Raise domain errors if this key cannot be used for authentication.
84-
85-
Raises:
86-
ApiKeyDisabledError: If the key is disabled.
87-
ApiKeyExpiredError: If the key is expired.
88-
"""
89-
...
90-
91-
92-
D = TypeVar("D", bound=ApiKeyEntity)
93-
94-
9514
def _normalize_datetime(value: Optional[datetime]) -> Optional[datetime]:
9615
"""Ensure datetimes are timezone-aware (UTC)."""
9716
if value is None:

src/fastapi_api_key/repositories/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABC, abstractmethod
22
from typing import Generic, Optional, List
33

4-
from fastapi_api_key.domain.entities import D
4+
from fastapi_api_key.domain.base import D
55

66

77
class AbstractApiKeyRepository(ABC, Generic[D]):

src/fastapi_api_key/repositories/in_memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Optional, List
22

3+
from fastapi_api_key.domain.base import D
34
from fastapi_api_key.repositories.base import AbstractApiKeyRepository
4-
from fastapi_api_key.domain.entities import D
55

66

77
class InMemoryApiKeyRepository(AbstractApiKeyRepository[D]):

src/fastapi_api_key/repositories/sql.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from fastapi_api_key.domain.base import D
2+
13
try:
24
import sqlalchemy # noqa: F401
35
except ModuleNotFoundError as e:
@@ -15,7 +17,7 @@
1517
from sqlalchemy.ext.asyncio import AsyncSession
1618
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
1719

18-
from fastapi_api_key.domain.entities import ApiKey, D
20+
from fastapi_api_key.domain.entities import ApiKey
1921
from fastapi_api_key.repositories.base import AbstractApiKeyRepository
2022
from fastapi_api_key.utils import datetime_factory
2123

src/fastapi_api_key/services/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)