Skip to content

Commit 1136eca

Browse files
committed
feat(experimental): remove pydantic
1 parent 5b9ab15 commit 1136eca

File tree

9 files changed

+146
-240
lines changed

9 files changed

+146
-240
lines changed

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ classifiers = [
2525
]
2626
dependencies = [
2727
'itsdangerous >=2.0.1,<3.0.0',
28-
'pydantic >=2.0.0',
29-
'pydantic-settings >=2.0.0',
3028
'starlette >=0',
3129
]
3230
description = 'Stateless implementation of Cross-Site Request Forgery (XSRF) Protection by using Double Submit Cookie mitigation pattern'

src/fastapi_csrf_protect/core.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111

1212
### Standard packages ###
1313
from hashlib import sha1
14+
from json import loads
1415
from re import match
1516
from os import urandom
1617
from typing import Dict, Optional, Tuple, Union
1718

1819
### Third-party packages ###
1920
from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer
20-
from pydantic import create_model
2121
from starlette.datastructures import Headers, UploadFile
2222
from starlette.requests import Request
2323
from starlette.responses import Response
@@ -56,11 +56,13 @@ def get_csrf_from_body(self, data: bytes) -> str:
5656
:param data: attached request body containing cookie data with configured `token_key`
5757
:type data: bytes
5858
"""
59-
fields: Dict[str, Tuple[type, str]] = {self._token_key: (str, "csrf-token")}
60-
Body = create_model("Body", **fields)
6159
content: str = '{"' + data.decode("utf-8").replace("&", '","').replace("=", '":"') + '"}'
62-
body = Body.model_validate_json(content)
63-
token: str = body.model_dump()[self._token_key]
60+
body: Dict[str, str] = loads(content)
61+
token: str = ""
62+
try:
63+
token = body[self._token_key]
64+
except AttributeError:
65+
pass
6466
return token
6567

6668
def get_csrf_from_headers(self, headers: Headers) -> str:

src/fastapi_csrf_protect/csrf_config.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010
# *************************************************************
1111

1212
### Standard packages ###
13-
from typing import Any, ClassVar, Callable, Literal, Optional, Sequence, Set, Tuple, Union
14-
15-
### Third-party packages ###
16-
from pydantic import ValidationError
17-
from pydantic_settings import BaseSettings
13+
from typing import Any, ClassVar, Callable, Literal, Optional, Sequence, Set, Tuple, cast
1814

1915
### Local modules ###
2016
from fastapi_csrf_protect.load_config import LoadConfig
@@ -30,7 +26,7 @@ class CsrfConfig(object):
3026
_header_type: ClassVar[Optional[str]] = None
3127
_httponly: ClassVar[bool] = True
3228
_max_age: ClassVar[int] = 3600
33-
_methods: ClassVar[Set[Literal["DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT"]]] = {
29+
_methods: ClassVar[Set[str]] = {
3430
"POST",
3531
"PUT",
3632
"PATCH",
@@ -41,15 +37,14 @@ class CsrfConfig(object):
4137
_token_key: ClassVar[str] = "csrf-token"
4238

4339
@classmethod
44-
def load_config(
45-
cls, settings: Callable[..., Union[Sequence[Tuple[str, Any]], BaseSettings]]
46-
) -> None:
40+
def load_config(cls, settings: Callable[..., Sequence[Tuple[str, Any]]]) -> None:
4741
try:
4842
config = LoadConfig(**{key.lower(): value for key, value in settings()})
4943
cls._cookie_key = config.cookie_key or cls._cookie_key
5044
cls._cookie_path = config.cookie_path or cls._cookie_path
5145
cls._cookie_domain = config.cookie_domain
52-
cls._cookie_samesite = config.cookie_samesite
46+
if config.cookie_samesite in {"lax", "strict", "none"}:
47+
cls._cookie_samesite = cast(Literal["lax", "strict", "none"], config.cookie_samesite)
5348
cls._cookie_secure = False if config.cookie_secure is None else config.cookie_secure
5449
cls._header_name = config.header_name or cls._header_name
5550
cls._header_type = config.header_type
@@ -59,11 +54,11 @@ def load_config(
5954
cls._secret_key = config.secret_key
6055
cls._token_location = config.token_location or cls._token_location
6156
cls._token_key = config.token_key or cls._token_key
62-
except ValidationError:
57+
except ValueError:
6358
raise
6459
except Exception as err:
6560
print(err)
66-
raise TypeError('CsrfConfig must be pydantic "BaseSettings" or list of tuple')
61+
raise TypeError("CsrfConfig must be a sequence of tuples")
6762

6863

6964
__all__: Tuple[str, ...] = ("CsrfConfig",)

src/fastapi_csrf_protect/load_config.py

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,79 @@
1010
# *************************************************************
1111

1212
### Standard packages ###
13-
from typing import Literal, Optional, Set, Tuple
13+
from typing import Optional, List, Set, Tuple, Union, get_args, get_origin
1414

1515
### Third-party packages ###
16-
from pydantic import (
17-
BaseModel,
18-
StrictBool,
19-
StrictInt,
20-
StrictStr,
21-
model_validator,
22-
)
23-
24-
25-
class LoadConfig(BaseModel):
26-
cookie_key: Optional[StrictStr] = "fastapi-csrf-token"
27-
cookie_path: Optional[StrictStr] = "/"
28-
cookie_domain: Optional[StrictStr] = None
29-
cookie_samesite: Optional[Literal["lax", "none", "strict"]] = "lax"
30-
cookie_secure: Optional[StrictBool] = False
31-
header_name: Optional[StrictStr] = "X-CSRF-Token"
32-
header_type: Optional[StrictStr] = None
33-
httponly: Optional[StrictBool] = True
34-
max_age: Optional[StrictInt] = 3600
35-
methods: Optional[Set[Literal["DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT"]]] = None
36-
secret_key: Optional[StrictStr] = None
37-
token_location: Optional[Literal["body", "header"]] = "header"
38-
token_key: Optional[StrictStr] = None
39-
40-
@model_validator(mode="after")
41-
def validate_cookie_samesite_none_secure(self) -> "LoadConfig":
16+
from dataclasses import dataclass
17+
18+
19+
@dataclass
20+
class LoadConfig:
21+
cookie_key: Optional[str] = "fastapi-csrf-token"
22+
cookie_path: Optional[str] = "/"
23+
cookie_domain: Optional[str] = None
24+
cookie_samesite: Optional[str] = "lax"
25+
cookie_secure: Optional[bool] = False
26+
header_name: Optional[str] = "X-CSRF-Token"
27+
header_type: Optional[str] = None
28+
httponly: Optional[bool] = True
29+
max_age: Optional[int] = 3600
30+
methods: Optional[Set[str]] = None
31+
secret_key: Optional[str] = None
32+
token_location: Optional[str] = "header"
33+
token_key: Optional[str] = None
34+
35+
def __post_init__(self) -> None:
36+
self.validate_attribute_types()
37+
self.validate_cookie_samesite()
38+
self.validate_cookie_samesite_none_secure()
39+
self.validate_methods()
40+
self.validate_token_key()
41+
self.validate_token_location()
42+
43+
def validate_attribute_types(self) -> None:
44+
for name, field_type in self.__annotations__.items():
45+
origin = get_origin(field_type)
46+
if origin == Union:
47+
types = get_args(field_type)
48+
typed: List[bool] = []
49+
for current_type in types:
50+
if get_origin(current_type) is None:
51+
typed.append(isinstance(getattr(self, name), current_type))
52+
else:
53+
subscripted = get_origin(current_type)
54+
typed.append(isinstance(getattr(self, name), subscripted))
55+
# TODO: subtypes
56+
if not any(typed):
57+
raise TypeError(f"The field `{name}` was not correctly assigned as `{field_type}`.")
58+
elif not isinstance(getattr(self, name), field_type):
59+
current_type = type(getattr(self, name))
60+
raise TypeError(
61+
f"The field `{name}` was assigned by `{current_type}` instead of `{field_type}`"
62+
)
63+
64+
def validate_methods(self) -> None:
65+
if self.methods is not None and isinstance(self.methods, set):
66+
for method in self.methods:
67+
if method not in {"DELETE", "GET", "PATCH", "POST", "PUT"}:
68+
raise TypeError("lol")
69+
70+
def validate_cookie_samesite(self) -> None:
71+
if self.cookie_samesite is not None and self.cookie_samesite not in {"lax", "none", "strict"}:
72+
raise TypeError("lol")
73+
74+
def validate_cookie_samesite_none_secure(self) -> None:
4275
if self.cookie_samesite in {None, "none"} and self.cookie_secure is not True:
43-
raise ValueError('The "cookie_secure" must be True if "cookie_samesite" set to "none".')
44-
return self
76+
raise TypeError('The "cookie_secure" must be True if "cookie_samesite" set to "none".')
4577

46-
@model_validator(mode="after")
47-
def validate_token_key(self) -> "LoadConfig":
78+
def validate_token_key(self) -> None:
4879
token_location: str = self.token_location if self.token_location is not None else "header"
49-
if token_location == "body":
50-
if self.token_key is None:
51-
raise ValueError('The "token_key" must be present when "token_location" is "body"')
52-
return self
80+
if token_location == "body" and self.token_key is None:
81+
raise TypeError('The "token_key" must be present when "token_location" is "body"')
82+
83+
def validate_token_location(self) -> None:
84+
if self.token_location not in {"body", "header"}:
85+
raise TypeError("lol")
5386

5487

5588
__all__: Tuple[str, ...] = ("LoadConfig",)

src/pydantic.pyi

Lines changed: 0 additions & 80 deletions
This file was deleted.

src/pydantic_settings.pyi

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)