fastapi-ext-pkg 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/PKG-INFO +2 -1
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_ext_pkg.egg-info/PKG-INFO +2 -1
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_ext_pkg.egg-info/SOURCES.txt +7 -1
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_ext_pkg.egg-info/requires.txt +1 -0
- fastapi_ext_pkg-0.4.0/fastapi_extras/databases/redis.py +27 -0
- fastapi_ext_pkg-0.4.0/fastapi_extras/middlewares/__init__.py +0 -0
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_extras/middlewares/remove_path.py +5 -3
- fastapi_ext_pkg-0.4.0/fastapi_extras/orm/__init__.py +0 -0
- fastapi_ext_pkg-0.4.0/fastapi_extras/orm/sqlalchemy.py +27 -0
- fastapi_ext_pkg-0.4.0/fastapi_extras/security/__init__.py +0 -0
- fastapi_ext_pkg-0.4.0/fastapi_extras/security/auth.py +97 -0
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/pyproject.toml +6 -2
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/README.md +0 -0
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_ext_pkg.egg-info/dependency_links.txt +0 -0
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_ext_pkg.egg-info/top_level.txt +0 -0
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_extras/__init__.py +0 -0
- {fastapi_ext_pkg-0.2.0/fastapi_extras/middlewares → fastapi_ext_pkg-0.4.0/fastapi_extras/databases}/__init__.py +0 -0
- {fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-ext-pkg
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A well-tailored utility set for FastAPI.
|
|
5
5
|
Author-email: Dotz Developers <devs-dotz@dotz.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
|
|
@@ -19,6 +19,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
19
19
|
Requires-Python: >=3.8
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
Requires-Dist: fastapi
|
|
22
|
+
Requires-Dist: httpx
|
|
22
23
|
|
|
23
24
|
# FastAPI Extension Package
|
|
24
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-ext-pkg
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A well-tailored utility set for FastAPI.
|
|
5
5
|
Author-email: Dotz Developers <devs-dotz@dotz.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
|
|
@@ -19,6 +19,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
19
19
|
Requires-Python: >=3.8
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
Requires-Dist: fastapi
|
|
22
|
+
Requires-Dist: httpx
|
|
22
23
|
|
|
23
24
|
# FastAPI Extension Package
|
|
24
25
|
|
|
@@ -6,5 +6,11 @@ fastapi_ext_pkg.egg-info/dependency_links.txt
|
|
|
6
6
|
fastapi_ext_pkg.egg-info/requires.txt
|
|
7
7
|
fastapi_ext_pkg.egg-info/top_level.txt
|
|
8
8
|
fastapi_extras/__init__.py
|
|
9
|
+
fastapi_extras/databases/__init__.py
|
|
10
|
+
fastapi_extras/databases/redis.py
|
|
9
11
|
fastapi_extras/middlewares/__init__.py
|
|
10
|
-
fastapi_extras/middlewares/remove_path.py
|
|
12
|
+
fastapi_extras/middlewares/remove_path.py
|
|
13
|
+
fastapi_extras/orm/__init__.py
|
|
14
|
+
fastapi_extras/orm/sqlalchemy.py
|
|
15
|
+
fastapi_extras/security/__init__.py
|
|
16
|
+
fastapi_extras/security/auth.py
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, AsyncGenerator, Optional, Union
|
|
2
|
+
|
|
3
|
+
import redis.asyncio as redis
|
|
4
|
+
from pydantic import RedisDsn
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Redis(redis.Redis):
|
|
8
|
+
async def get(self, key: str, *args: Any, **kwargs: Any) -> Optional[str]:
|
|
9
|
+
return await super().get(key, *args, **kwargs)
|
|
10
|
+
|
|
11
|
+
async def set(
|
|
12
|
+
self, key: str, value: str, ttl: Optional[int] = None, *args: Any, **kwargs: Any
|
|
13
|
+
) -> None:
|
|
14
|
+
await super().set(key, value, ttl, *args, **kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RedisManager:
|
|
18
|
+
def __init__(self, url: Union[RedisDsn, str]):
|
|
19
|
+
self.pool = redis.ConnectionPool.from_url(str(url))
|
|
20
|
+
|
|
21
|
+
async def __call__(self) -> AsyncGenerator[Redis, None]:
|
|
22
|
+
cli = Redis(connection_pool=self.pool)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
yield cli
|
|
26
|
+
finally:
|
|
27
|
+
await cli.aclose()
|
|
File without changes
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class RemovePathMiddleware:
|
|
5
7
|
def __init__(self, app: ASGIApp, path: str = "") -> None:
|
|
6
8
|
self.app = app
|
|
7
|
-
self.path = path
|
|
9
|
+
self.path = path.strip()
|
|
8
10
|
|
|
9
11
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
10
|
-
if scope["type"] != "http":
|
|
12
|
+
if scope["type"] != "http" or self.path == "":
|
|
11
13
|
return await self.app(scope, receive, send)
|
|
12
14
|
|
|
13
|
-
scope["path"] =
|
|
15
|
+
scope["path"] = re.sub(f"^({self.path})(/.*)?$", r"\g<2>", scope["path"])
|
|
14
16
|
|
|
15
17
|
await self.app(scope, receive, send)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, Generator, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import AnyUrl
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Database:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
database_url: Union[str, AnyUrl],
|
|
12
|
+
autocommit: bool = False,
|
|
13
|
+
autoflush: bool = False,
|
|
14
|
+
**kwargs: Any,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.engine = create_engine(str(database_url), **kwargs)
|
|
17
|
+
self.sessionmaker = sessionmaker(
|
|
18
|
+
autocommit=autocommit, autoflush=autoflush, bind=self.engine
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def __call__(self) -> Generator[Session, None, None]:
|
|
22
|
+
session = self.sessionmaker()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
yield session
|
|
26
|
+
finally:
|
|
27
|
+
session.close()
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
from typing import Any, AsyncGenerator, Awaitable, Callable, Optional, Protocol, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from fastapi import Depends, HTTPException, Request, status
|
|
9
|
+
from fastapi.security import APIKeyCookie, APIKeyHeader, APIKeyQuery
|
|
10
|
+
from pydantic import AnyHttpUrl
|
|
11
|
+
from typing_extensions import Annotated
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
UNAUTHORIZED = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
|
16
|
+
DEFAULT_SCHEME = APIKeyHeader(name="Authorization")
|
|
17
|
+
DEFAULT_CACHE_TTL = 60 # seconds
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def DEFAULT_CACHE_GEN() -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@typing.runtime_checkable
|
|
25
|
+
class Cache(Protocol):
|
|
26
|
+
async def get(self, key: str) -> Optional[str]: ...
|
|
27
|
+
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
APIKeyScheme = Union[APIKeyCookie, APIKeyHeader, APIKeyQuery]
|
|
31
|
+
Authorizer = Callable[[Request, str, Optional[Cache]], Awaitable[Any]]
|
|
32
|
+
CacheGenerator = Callable[..., Union[Cache, AsyncGenerator[Cache, None], None]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def keygen(seed: str, prefix: str = "") -> str:
|
|
36
|
+
hash = hashlib.sha256(seed.encode()).hexdigest()
|
|
37
|
+
return f"{prefix}{hash}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def remote_authorization(
|
|
41
|
+
url: Union[str, AnyHttpUrl],
|
|
42
|
+
*,
|
|
43
|
+
scheme: APIKeyScheme = DEFAULT_SCHEME,
|
|
44
|
+
cache_gen: CacheGenerator = DEFAULT_CACHE_GEN,
|
|
45
|
+
cache_ttl: int = DEFAULT_CACHE_TTL,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
) -> Authorizer:
|
|
48
|
+
assert isinstance(scheme, (APIKeyCookie, APIKeyHeader, APIKeyQuery)), "Invalid APIKeyScheme"
|
|
49
|
+
# TODO: validate cachegen argument using introspection and type annotations
|
|
50
|
+
|
|
51
|
+
async def authorizer(
|
|
52
|
+
request: Request,
|
|
53
|
+
token: Annotated[str, Depends(scheme)],
|
|
54
|
+
cache: Annotated[Optional[Cache], Depends(cache_gen)],
|
|
55
|
+
) -> Any:
|
|
56
|
+
key = keygen(token, prefix="authorizer:")
|
|
57
|
+
cached = await cache.get(key) if cache else None
|
|
58
|
+
|
|
59
|
+
if cached:
|
|
60
|
+
cached = json.loads(cached)
|
|
61
|
+
|
|
62
|
+
if cached and isinstance(cached, dict):
|
|
63
|
+
if not cached.get("authorized"):
|
|
64
|
+
raise UNAUTHORIZED
|
|
65
|
+
|
|
66
|
+
request.scope["authorizer"] = cached.get("context")
|
|
67
|
+
return request.scope["authorizer"]
|
|
68
|
+
|
|
69
|
+
info = {
|
|
70
|
+
"method": request.method,
|
|
71
|
+
"path": request.url.path,
|
|
72
|
+
"query_params": dict(request.query_params),
|
|
73
|
+
"headers": dict(request.headers),
|
|
74
|
+
"cookies": request.cookies,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async with httpx.AsyncClient() as client:
|
|
78
|
+
try:
|
|
79
|
+
response = await client.post(str(url), json=info, **kwargs)
|
|
80
|
+
except httpx.HTTPError as http_error:
|
|
81
|
+
logger.error(http_error)
|
|
82
|
+
raise UNAUTHORIZED
|
|
83
|
+
else:
|
|
84
|
+
authorized = response.status_code == status.HTTP_200_OK
|
|
85
|
+
context = response.json()
|
|
86
|
+
|
|
87
|
+
if cache:
|
|
88
|
+
value = json.dumps({"authorized": authorized, "context": context})
|
|
89
|
+
await cache.set(key, value, cache_ttl)
|
|
90
|
+
|
|
91
|
+
if not authorized:
|
|
92
|
+
raise UNAUTHORIZED
|
|
93
|
+
|
|
94
|
+
request.scope["authorizer"] = context
|
|
95
|
+
return request.scope["authorizer"]
|
|
96
|
+
|
|
97
|
+
return authorizer
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-ext-pkg"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "A well-tailored utility set for FastAPI."
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
authors = [
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
"License :: OSI Approved :: MIT License",
|
|
26
26
|
]
|
|
27
27
|
requires-python = ">=3.8"
|
|
28
|
-
dependencies = ["fastapi"]
|
|
28
|
+
dependencies = ["fastapi", "httpx"]
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Repository = "https://github.com/DotzInc/fastapi-ext-pkg"
|
|
@@ -53,3 +53,7 @@ quote-style = "double"
|
|
|
53
53
|
indent-style = "space"
|
|
54
54
|
skip-magic-trailing-comma = false
|
|
55
55
|
line-ending = "auto"
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
addopts = "--disable-warnings -x"
|
|
59
|
+
testpaths = ["tests/"]
|
|
File without changes
|
{fastapi_ext_pkg-0.2.0 → fastapi_ext_pkg-0.4.0}/fastapi_ext_pkg.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|