Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v1.8.0 #152

Merged
merged 9 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test_and_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:

env:
DEFAULT_LINUX: "slim"
DEFAULT_PYTHON: "3.12"
DEFAULT_PYTHON: "3.13"
DEFAULT_SCHEMAS: "pydantic"

jobs:
Expand All @@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
python: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
container:
image: python:${{ matrix.python }}
steps:
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
strategy:
matrix:
linux: ["slim"]
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
schemas: ["pydantic", "marshmallow", "typesystem"]
steps:
- name: Check out the repo
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_pull_request_branch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
container:
image: python:${{ matrix.python }}
steps:
Expand Down
10 changes: 9 additions & 1 deletion flama/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ class URLComponent(Component):
def resolve(self, scope: types.Scope) -> types.URL:
host, port = scope.get("server", ["", None])
scheme = scope.get("scheme", "")
path = scope.get("path", "")
query = scope.get("query_string", b"").decode()

if (scheme == "http" and port in (80, None)) or (scheme == "https" and port in (443, None)):
port = None

return types.URL(f"{scheme}://{host}{f':{port}' if port else ''}{scope.get('raw_path', b'').decode()}")
if port:
host += f":{port}"

if query:
path += f"?{query}"

return types.URL(f"{scheme}://{host}{path}")


class SchemeComponent(Component):
Expand Down
50 changes: 38 additions & 12 deletions flama/authentication/components.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import http
import logging
import typing as t

from flama import Component
from flama.authentication import exceptions, jwt
from flama.authentication import exceptions, jwt, types
from flama.exceptions import HTTPException
from flama.types import Headers
from flama.types.http import Cookies

logger = logging.getLogger(__name__)

__all__ = ["JWTComponent"]
__all__ = ["AccessTokenComponent", "RefreshTokenComponent"]


class JWTComponent(Component):
def __init__(
self,
secret: bytes,
*,
header_key: str = "Authorization",
header_prefix: str = "Bearer",
cookie_key: str = "flama_authentication",
):
class BaseTokenComponent(Component):
def __init__(self, secret: bytes, *, header_key: str, header_prefix: str, cookie_key: str):
self.secret = secret
self.header_key = header_key
self.header_prefix = header_prefix
self.cookie_key = cookie_key

def _token_from_cookies(self, cookies: Cookies) -> bytes:
print(f"ERROR: {cookies}")
try:
token = cookies[self.cookie_key]["value"]
except KeyError:
Expand All @@ -36,6 +31,7 @@ def _token_from_cookies(self, cookies: Cookies) -> bytes:
return token.encode()

def _token_from_header(self, headers: Headers) -> bytes:
print(f"ERROR: {headers}")
try:
header_prefix, token = headers[self.header_key].split()
except KeyError:
Expand All @@ -55,7 +51,7 @@ def _token_from_header(self, headers: Headers) -> bytes:

return token.encode()

def resolve(self, headers: Headers, cookies: Cookies) -> jwt.JWT:
def _resolve_token(self, headers: Headers, cookies: Cookies) -> jwt.JWT:
try:
try:
encoded_token = self._token_from_header(headers)
Expand All @@ -76,3 +72,33 @@ def resolve(self, headers: Headers, cookies: Cookies) -> jwt.JWT:
)

return token


class AccessTokenComponent(BaseTokenComponent):
def __init__(
self,
secret: bytes,
*,
header_prefix: str = "Bearer",
header_key: str = "access_token",
cookie_key: str = "access_token",
):
super().__init__(secret, header_prefix=header_prefix, header_key=header_key, cookie_key=cookie_key)

def resolve(self, headers: Headers, cookies: Cookies) -> types.AccessToken:
return t.cast(types.AccessToken, self._resolve_token(headers, cookies))


class RefreshTokenComponent(BaseTokenComponent):
def __init__(
self,
secret: bytes,
*,
header_prefix: str = "Bearer",
header_key: str = "refresh_token",
cookie_key: str = "refresh_token",
):
super().__init__(secret, header_prefix=header_prefix, header_key=header_key, cookie_key=cookie_key)

def resolve(self, headers: Headers, cookies: Cookies) -> types.RefreshToken:
return t.cast(types.RefreshToken, self._resolve_token(headers, cookies))
6 changes: 3 additions & 3 deletions flama/authentication/jwt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from flama.authentication.jwt.jwt import JWT

__all__ = ["JWT"]
from flama.authentication.components import * # noqa
from flama.authentication.jwt.jwt import JWT # noqa
from flama.authentication.types import * # noqa
6 changes: 4 additions & 2 deletions flama/authentication/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import typing as t

from flama.authentication.jwt.jwt import JWT
from flama import authentication
from flama.exceptions import HTTPException
from flama.http import APIErrorResponse, Request

Expand Down Expand Up @@ -44,7 +44,9 @@ async def _get_response(self, scope: "types.Scope", receive: "types.Receive") ->
return self.app

try:
token: JWT = await app.injector.resolve(JWT).value({"request": Request(scope, receive=receive)})
token: authentication.AccessToken = await app.injector.resolve(authentication.AccessToken).value(
{"request": Request(scope, receive=receive)}
)
except HTTPException as e:
logger.debug("JWT error: %s", e.detail)
return APIErrorResponse(status_code=e.status_code, detail=e.detail)
Expand Down
8 changes: 8 additions & 0 deletions flama/authentication/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import typing as t

from flama.authentication.jwt import JWT

__all__ = ["AccessToken", "RefreshToken"]

AccessToken = t.NewType("AccessToken", JWT)
RefreshToken = t.NewType("RefreshToken", JWT)
2 changes: 1 addition & 1 deletion flama/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def __init__(
self.lifespan = LifespanContextManager(app) if app else None
self.app = app

kwargs["app"] = app
kwargs.setdefault("transport", httpx.ASGITransport(app=app) if app else None) # type: ignore
kwargs.setdefault("base_url", "http://localapp")
kwargs["headers"] = {"user-agent": f"flama/{importlib.metadata.version('flama')}", **kwargs.get("headers", {})}

Expand Down
16 changes: 2 additions & 14 deletions flama/concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@
import sys
import typing as t

if sys.version_info < (3, 9): # PORT: Remove when stop supporting 3.8 # pragma: no cover
import contextvars

async def to_thread(func, /, *args, **kwargs):
return await asyncio.get_running_loop().run_in_executor(
None, functools.partial(contextvars.copy_context().run, func, *args, **kwargs)
)

asyncio.to_thread = to_thread # pyright: ignore

if sys.version_info < (3, 10): # PORT: Remove when stop supporting 3.9 # pragma: no cover
from typing_extensions import ParamSpec, TypeGuard

Expand All @@ -39,7 +29,7 @@ def is_async(
while isinstance(obj, functools.partial):
obj = obj.func

return asyncio.iscoroutinefunction(obj) or (callable(obj) and asyncio.iscoroutinefunction(obj.__call__))
return asyncio.iscoroutinefunction(obj) or asyncio.iscoroutinefunction(getattr(obj, "__call__"))


async def run(
Expand All @@ -58,9 +48,7 @@ async def run(
if is_async(func):
return await func(*args, **kwargs)

return t.cast(
R, await asyncio.to_thread(func, *args, **kwargs) # PORT: Remove when stop supporting 3.8 # type: ignore
)
return t.cast(R, await asyncio.to_thread(func, *args, **kwargs))


if sys.version_info < (3, 11): # PORT: Remove when stop supporting 3.10 # pragma: no cover
Expand Down
14 changes: 14 additions & 0 deletions flama/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"WebSocketException",
"NotFoundException",
"MethodNotAllowedException",
"FrameworkNotInstalled",
"FrameworkVersionWarning",
]


Expand Down Expand Up @@ -139,3 +141,15 @@ def __repr__(self) -> str:
params = ("path", "params", "method", "allowed")
formatted_params = ", ".join([f"{x}={getattr(self, x)}" for x in params if getattr(self, x)])
return f"{self.__class__.__name__}({formatted_params})"


class FrameworkNotInstalled(Exception):
"""Cannot find an installed version of the framework."""

...


class FrameworkVersionWarning(Warning):
"""Warning for when a framework version does not match."""

...
3 changes: 2 additions & 1 deletion flama/models/models/pytorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

class PyTorchModel(Model):
def predict(self, x: t.List[t.List[t.Any]]) -> t.Any:
assert torch is not None, "`torch` must be installed to use PyTorchModel."
if torch is None: # noqa
raise exceptions.FrameworkNotInstalled("pytorch")

try:
return self.model(torch.Tensor(x)).tolist()
Expand Down
8 changes: 8 additions & 0 deletions flama/models/models/sklearn.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@
from flama import exceptions
from flama.models.base import Model

try:
import sklearn # type: ignore
except Exception: # pragma: no cover
sklearn = None


class SKLearnModel(Model):
def predict(self, x: t.List[t.List[t.Any]]) -> t.Any:
if sklearn is None: # noqa
raise exceptions.FrameworkNotInstalled("scikit-learn")

try:
return self.model.predict(x).tolist()
except ValueError as e:
Expand Down
13 changes: 11 additions & 2 deletions flama/models/models/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from flama import exceptions
from flama.models.base import Model

try:
import numpy as np # type: ignore
except Exception: # pragma: no cover
np = None

try:
import tensorflow as tf # type: ignore
except Exception: # pragma: no cover
Expand All @@ -11,9 +16,13 @@

class TensorFlowModel(Model):
def predict(self, x: t.List[t.List[t.Any]]) -> t.Any:
assert tf is not None, "`tensorflow` must be installed to use TensorFlowModel."
if np is None: # noqa
raise exceptions.FrameworkNotInstalled("numpy")

if tf is None: # noqa
raise exceptions.FrameworkNotInstalled("tensorflow")

try:
return self.model.predict(x).tolist()
return self.model.predict(np.array(x)).tolist()
except (tf.errors.OpError, ValueError): # type: ignore
raise exceptions.HTTPException(status_code=400)
Loading
Loading