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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-ext-pkg
3
- Version: 0.2.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.2.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()
@@ -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 if not path.endswith("/") else path[:-1]
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"] = scope["path"].replace(self.path, "", 1)
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()
@@ -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.2.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/"]