add documentation for some classes and functions
This commit is contained in:
parent
a8e7813318
commit
2a04991e5c
|
@ -3,22 +3,18 @@ __version__ = "0.1.0"
|
|||
|
||||
from .application import Application, StaticHandler
|
||||
from .client import Client
|
||||
from .enums import HttpStatus, SassOutputStyle
|
||||
from .enums import HttpStatus, PridePalette, SassOutputStyle
|
||||
from .error import HttpError
|
||||
from .request import Request
|
||||
from .response import Response, FileResponse, TemplateResponse
|
||||
from .runner import Runner, UvicornRunner, GranianRunner
|
||||
from .signal import Signal
|
||||
from .template import SassExtension, Template
|
||||
|
||||
from .misc import (
|
||||
Color,
|
||||
Cookie,
|
||||
StateProxy,
|
||||
Stream,
|
||||
convert_to_bytes,
|
||||
convert_to_string,
|
||||
is_loop_running
|
||||
Stream
|
||||
)
|
||||
|
||||
from .router import (
|
||||
|
@ -44,5 +40,4 @@ from .misc import ReaderFunction, WriterFunction
|
|||
from .request import AsgiVersionType, ScopeType
|
||||
from .router import RouteHandler
|
||||
from .runner import BackgroundTask
|
||||
from .signal import SignalCallback
|
||||
from .template import TemplateContextCallback
|
||||
|
|
|
@ -2,8 +2,9 @@ from __future__ import annotations
|
|||
|
||||
import traceback
|
||||
|
||||
from blib import Signal
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from mimetypes import guess_type
|
||||
from os.path import normpath
|
||||
from pathlib import Path
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
@ -14,7 +15,6 @@ from .misc import StateProxy, Stream, ReaderFunction, WriterFunction
|
|||
from .request import Request, ScopeType
|
||||
from .response import FileResponse, Response
|
||||
from .router import Router, RouteHandler, get_router
|
||||
from .signal import Signal
|
||||
from .template import Template, TemplateContextCallback
|
||||
|
||||
|
||||
|
@ -28,6 +28,8 @@ APPLICATIONS: dict[str, Application] = {} # type: ignore[type-arg]
|
|||
|
||||
|
||||
class Application(Generic[R, RT, AT]):
|
||||
"Represents an ASGI application"
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
template_search: list[Path | str] | None = None,
|
||||
|
@ -36,34 +38,65 @@ class Application(Generic[R, RT, AT]):
|
|||
request_class: type[R] = Request, # type: ignore[assignment]
|
||||
request_state_class: type[RT] = StateProxy, # type: ignore[assignment]
|
||||
app_state_class: type[AT] = StateProxy) -> None: # type: ignore[assignment]
|
||||
"""
|
||||
Create a new ``Application``
|
||||
|
||||
:param name: Identifier for the application. This is used for the ``Server`` header
|
||||
and adding routes.
|
||||
:param template_search: Directories to search when locating a template
|
||||
:param template_env: Context data to include with every template render
|
||||
:param template_context: Function that gets called on every template render
|
||||
:param request_class: Class to use for requests
|
||||
:param request_state_class: Class to use for the :attr:`basgi.Request.state` property
|
||||
:param app_state_class: Class to use for the :attr:`basgi.Application.state` property
|
||||
"""
|
||||
|
||||
if name in APPLICATIONS:
|
||||
raise ValueError(f"Application with name '{name}' already exists")
|
||||
|
||||
APPLICATIONS[name] = self
|
||||
|
||||
self.name: str = name
|
||||
"Identifier for the application"
|
||||
|
||||
self.state: AT = app_state_class({})
|
||||
"Data to be stored with the application"
|
||||
|
||||
self.request_class: type[R] = request_class
|
||||
"Class to use for requests"
|
||||
|
||||
self.request_state_class: type[RT] = request_state_class
|
||||
"Class to use for the :attr:`basgi.Request.state` property"
|
||||
|
||||
# Not sure how to properly annotate `Exception` here, so just ignore the warnings
|
||||
self.error_handlers: dict[type[ExceptionType], ExceptionCallback] = { # type: ignore
|
||||
HttpError: self.handle_http_error
|
||||
HttpError: self._handle_http_error
|
||||
}
|
||||
"Functions to be called on specific errors"
|
||||
|
||||
self.router: Router = get_router(self.name)
|
||||
self.router.trim_last_slash = True
|
||||
"Contains all of the routes the application will handle"
|
||||
|
||||
self.client: Client = Client()
|
||||
"Customized httpx client"
|
||||
|
||||
self.template: Template = Template(
|
||||
*(template_search or []),
|
||||
context_function = template_context,
|
||||
**(template_env or {}))
|
||||
|
||||
APPLICATIONS[name] = self
|
||||
**(template_env or {})
|
||||
)
|
||||
"Template environment"
|
||||
|
||||
|
||||
@classmethod
|
||||
def get(cls: type[Application[R, RT, AT]], name: str) -> Application[R, RT, AT]:
|
||||
@staticmethod
|
||||
def get(name: str) -> Application[R, RT, AT]:
|
||||
"""
|
||||
Get an application by its name
|
||||
|
||||
:param name: Identifier of the application
|
||||
:raises KeyError: If an application by the specified name cannot be found
|
||||
"""
|
||||
|
||||
return APPLICATIONS[name]
|
||||
|
||||
|
||||
|
@ -94,7 +127,7 @@ class Application(Generic[R, RT, AT]):
|
|||
await self.on_request.handle_emit(request)
|
||||
|
||||
except Exception as error:
|
||||
response = self.handle_error(request, error)
|
||||
response = self._handle_error(request, error)
|
||||
|
||||
try:
|
||||
match = self.router(request.path, request.method)
|
||||
|
@ -105,13 +138,13 @@ class Application(Generic[R, RT, AT]):
|
|||
raise HttpError(500, "Empty response")
|
||||
|
||||
except Exception as error:
|
||||
response = self.handle_error(request, error)
|
||||
response = self._handle_error(request, error)
|
||||
|
||||
try:
|
||||
await self.on_response.handle_emit(request, response)
|
||||
|
||||
except Exception as error:
|
||||
response = self.handle_error(request, error)
|
||||
response = self._handle_error(request, error)
|
||||
|
||||
if not response.headers.get("Server"):
|
||||
response.headers["Server"] = self.name
|
||||
|
@ -122,34 +155,79 @@ class Application(Generic[R, RT, AT]):
|
|||
|
||||
@Signal(5.0)
|
||||
async def on_request(self, request: Request) -> None:
|
||||
pass
|
||||
"""
|
||||
:class:`blib.Signal` that gets emitted just after the request is created
|
||||
|
||||
:param application: The application that emit the signal
|
||||
:param request: The newly-created request
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@Signal(5.0)
|
||||
async def on_response(self, request: Request, response: Response) -> None:
|
||||
pass
|
||||
"""
|
||||
:class:`blib.Signal` that gets emitted just after the route handler is called
|
||||
|
||||
:param application: The application that emit the signal
|
||||
:param request: The request
|
||||
:param response: Response from the route handler
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@Signal(5.0)
|
||||
async def on_startup(self) -> None:
|
||||
pass
|
||||
"""
|
||||
:class:`blib.Signal` that gets emitted on server start
|
||||
|
||||
:param application: The application that emit the signal
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@Signal(5.0)
|
||||
async def on_shutdown(self) -> None:
|
||||
pass
|
||||
"""
|
||||
:class:`blib.Signal` that gets emitted just before server stop
|
||||
|
||||
:param application: The application that emit the signal
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def add_route(self, method: str, path: str, handler: RouteHandler) -> None:
|
||||
"""
|
||||
Add a route handler
|
||||
|
||||
:param method: HTTP method the route will handle
|
||||
:param path: Virtual HTTP path the route will handle
|
||||
:param handler: Function to call when the route is requested
|
||||
"""
|
||||
|
||||
self.router.bind(handler, path, methods = [method])
|
||||
|
||||
|
||||
def add_static(self, path: str, location: Path | str, cached: bool = False) -> None:
|
||||
"""
|
||||
Add a route handler that servers a directory. If a directory is requested and
|
||||
``index.html`` exists in that directory, it will be served instead.
|
||||
|
||||
:param path: Virtual HTTP path the route will handle
|
||||
:param location: Directory to use for serving files
|
||||
:param cached: Store all requested files in memory after being read from the disk for
|
||||
the first time
|
||||
"""
|
||||
|
||||
handler = StaticHandler(path, location, cached)
|
||||
self.add_route("GET", handler.path, handler)
|
||||
|
||||
|
||||
def print_access_log(self, request: Request, response: Response) -> None:
|
||||
"""
|
||||
Prints the request line to the terminal. Override this in a sub-class to customize it.
|
||||
"""
|
||||
|
||||
message = "{}: {} \"{} {}\" {} {} \"{}\"".format(
|
||||
"INFO",
|
||||
request.headers.get(
|
||||
|
@ -169,32 +247,48 @@ class Application(Generic[R, RT, AT]):
|
|||
print(message, flush = True)
|
||||
|
||||
|
||||
def handle_error(self, request: Request, error: Exception) -> Response:
|
||||
def _handle_error(self, request: Request, error: Exception) -> Response:
|
||||
try:
|
||||
return self.error_handlers[type(error)](request, error)
|
||||
|
||||
except KeyError:
|
||||
traceback.print_exception(error)
|
||||
return Response(500, "Internal Server Error")
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return Response(500, "Internal Server Error")
|
||||
|
||||
|
||||
def handle_http_error(self, request: Request, error: HttpError) -> Response:
|
||||
def _handle_http_error(self, request: Request, error: HttpError) -> Response:
|
||||
return Response.new_from_http_error(error)
|
||||
|
||||
|
||||
|
||||
class StaticHandler:
|
||||
"Provides a handler for a directory of static files"
|
||||
|
||||
|
||||
def __init__(self, path: str, location: Path | str, cached: bool) -> None:
|
||||
"""
|
||||
Create a new static file handler
|
||||
|
||||
:param path: Virtual HTTP path the route will handle
|
||||
:param location: Directory to use for serving files
|
||||
:param cached: Store all requested files in memory after being read from the disk for
|
||||
the first time
|
||||
"""
|
||||
|
||||
if isinstance(location, str):
|
||||
location = Path(location)
|
||||
|
||||
self.path: str = path.rstrip("/") + "/{filepath:.*}"
|
||||
"Path regex used to determine if a path is handled or not"
|
||||
|
||||
self.location: Path = Path(location).expanduser().resolve()
|
||||
"Directory to use for serving files"
|
||||
|
||||
self.cached: bool = cached
|
||||
"Whether or not all requested files will be stored in memory"
|
||||
|
||||
self.cache: dict[str, Response] = {}
|
||||
"Currently cached responses"
|
||||
|
||||
if not self.location.exists():
|
||||
raise FileNotFoundError(self.location)
|
||||
|
@ -204,27 +298,32 @@ class StaticHandler:
|
|||
|
||||
|
||||
async def __call__(self, request: Request) -> Response:
|
||||
filepath = request.params["filepath"]
|
||||
"""
|
||||
Function that gets called for incoming requests
|
||||
|
||||
if self.cached:
|
||||
return await self.handle_call_cached(request, filepath)
|
||||
:param request: The incoming request
|
||||
:param filepath: Relative path of the requested file
|
||||
:raises HttpError: If the file on disk cannot be found
|
||||
"""
|
||||
|
||||
return await self.handle_call(request, filepath)
|
||||
filepath = normpath(request.params["filepath"].lstrip("/"))
|
||||
|
||||
if self.cached and filepath in self.cache:
|
||||
return self.cache[filepath]
|
||||
|
||||
async def handle_call(self, request: Request, filepath: str) -> Response:
|
||||
filepath = normpath(filepath)
|
||||
path = self.location.joinpath(filepath.lstrip("/"))
|
||||
path = self.location.joinpath(filepath)
|
||||
|
||||
if path.is_dir():
|
||||
path = path.joinpath("index.html")
|
||||
|
||||
if not path.is_file():
|
||||
raise HttpError(404, str(filepath))
|
||||
raise HttpError(404, filepath)
|
||||
|
||||
if self.cached:
|
||||
with path.open("rb") as fd:
|
||||
response = Response(200, fd.read(), mimetype = guess_type(path)[0])
|
||||
|
||||
self.cache[filepath] = response
|
||||
return response
|
||||
|
||||
return FileResponse(path)
|
||||
|
||||
|
||||
@lru_cache(maxsize = 128, typed = True)
|
||||
async def handle_call_cached(self, request: Request, filepath: str) -> Response:
|
||||
return await self.handle_call(request, filepath)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import re
|
||||
|
||||
from aputils import IntEnum, StrEnum
|
||||
from blib import Enum, IntEnum, StrEnum
|
||||
|
||||
|
||||
CAPITAL = re.compile("[A-Z][^A-Z]")
|
||||
|
@ -86,6 +86,25 @@ class HttpStatus(IntEnum):
|
|||
return " ".join(CAPITAL.findall(self.name))
|
||||
|
||||
|
||||
class PridePalette(tuple[str, ...], Enum):
|
||||
LGBT = ("#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000")
|
||||
LESBIAN = ("#D52D00", "#EF7627", "#FF9A56", "#FFFFFF", "#D162A4", "#B55690", "#A30262")
|
||||
BI = ("#D60270", "#9B4F96", "#0038A8")
|
||||
GAY = ("#078D70", "#26CEAA", "#98E8C1", "#FFFFFF", "#7BADE2", "#5049CC", "#3D1A78")
|
||||
PANSEXUAL = ("#FF218C", "#FFD800", "#21B1FF")
|
||||
ASEXUAL = ("#000000", "#A3A3A3", "#FFFFFF", "#800080")
|
||||
AROMANTIC = ("#3DA542", "#A7D379", "#FFFFFF", "#A9A9A9", "#000000")
|
||||
TRANS = ("#55CDFC", "#F7A8B8", "#FFFFFF")
|
||||
TRANS_BLACK = ("#55CDFC", "#F7A8B8", "#000000")
|
||||
TRANSMASC = ("#FF8ABD", "#CDF5FE", "#9AEBFF", "#74DFFF")
|
||||
NONBINARY = ("#FCF434", "#FFFFFF", "#9C59D1", "#2C2C2C")
|
||||
|
||||
# aliases
|
||||
ACE = ASEXUAL
|
||||
ARO = AROMANTIC
|
||||
ENBY = NONBINARY
|
||||
|
||||
|
||||
class SassOutputStyle(StrEnum):
|
||||
NESTED = "nested"
|
||||
EXPANDED = "expanded"
|
||||
|
|
339
basgi/misc.py
339
basgi/misc.py
|
@ -1,7 +1,4 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from aputils import HttpDate, JsonBase
|
||||
from aputils import HttpDate
|
||||
from colorsys import rgb_to_hls, hls_to_rgb
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
|
@ -10,7 +7,6 @@ from typing import Any, Literal, Self
|
|||
from collections.abc import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
ItemsView,
|
||||
KeysView,
|
||||
|
@ -19,116 +15,50 @@ from collections.abc import (
|
|||
ValuesView
|
||||
)
|
||||
|
||||
from .enums import PridePalette
|
||||
|
||||
|
||||
ReaderFunction = Callable[..., Awaitable[dict[str, Any]]]
|
||||
WriterFunction = Callable[[dict[str, Any]], Awaitable[None]]
|
||||
|
||||
|
||||
PRIDE_COLORS = {
|
||||
"lgbt": ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"],
|
||||
"lesbian": ["#D52D00", "#EF7627", "#FF9A56", "#FFFFFF", "#D162A4", "#B55690", "#A30262"],
|
||||
"bi": ["#D60270", "#9B4F96", "#0038A8"],
|
||||
"gay": ["#078D70", "#26CEAA", "#98E8C1", "#FFFFFF", "#7BADE2", "#5049CC", "#3D1A78"],
|
||||
"pan": ["#FF218C", "#FFD800", "#21B1FF"],
|
||||
"asexual": ["#000000", "#A3A3A3", "#FFFFFF", "#800080"],
|
||||
"aromantic": ["#3DA542", "#A7D379", "#FFFFFF", "#A9A9A9", "#000000"],
|
||||
"trans": ["#55CDFC", "#F7A8B8", "#FFFFFF"],
|
||||
"trans-black": ["#55CDFC", "#F7A8B8", "#000000"],
|
||||
"transmasc": ["#FF8ABD", "#CDF5FE", "#9AEBFF", "#74DFFF"],
|
||||
"non-binary": ["#FCF434", "#FFFFFF", "#9C59D1", "#2C2C2C"]
|
||||
}
|
||||
|
||||
|
||||
def convert_to_bytes(value: Any, encoding: str = "utf-8") -> bytes:
|
||||
"""
|
||||
Convert an object to :class:`bytes`
|
||||
|
||||
:param value: Object to be converted
|
||||
:param encoding: Character encoding to use if the object is a string or gets converted to
|
||||
one in the process
|
||||
:raises TypeError: If the object cannot be converted
|
||||
"""
|
||||
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
|
||||
try:
|
||||
return convert_to_string(value).encode(encoding)
|
||||
|
||||
except TypeError:
|
||||
raise TypeError(f"Cannot convert '{type(value).__name__}' into bytes") from None
|
||||
|
||||
|
||||
def convert_to_string(value: Any, encoding: str = 'utf-8') -> str:
|
||||
"""
|
||||
Convert an object to :class:`str`
|
||||
|
||||
:param value: Object to be converted
|
||||
:param encoding: Character encoding to use if the object is a :class:`bytes` object
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
return ''
|
||||
|
||||
if isinstance(value, bytes):
|
||||
return value.decode(encoding)
|
||||
|
||||
if isinstance(value, bool):
|
||||
return str(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
if isinstance(value, JsonBase):
|
||||
return value.to_json()
|
||||
|
||||
if isinstance(value, (dict, list, tuple, set)):
|
||||
return json.dumps(value)
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
|
||||
raise TypeError(f'Cannot convert "{type(value).__name__}" into a string') from None
|
||||
|
||||
|
||||
def is_loop_running() -> bool:
|
||||
"Check if an event loop is running in the current thread"
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
if not loop:
|
||||
return False
|
||||
|
||||
return loop.is_running()
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class Color(str):
|
||||
"Represents an HTML color value"
|
||||
|
||||
|
||||
def __new__(cls, color: str) -> Self:
|
||||
if not isinstance(color, str):
|
||||
raise TypeError("Color must be a valid hex string")
|
||||
"""
|
||||
Create a new ``Color`` object
|
||||
|
||||
if not color.startswith("#"):
|
||||
color = f"#{str(color)}"
|
||||
:param color: Hex color string of 3 or 6 characters (``#`` character optional)
|
||||
"""
|
||||
|
||||
if len(color) == 4:
|
||||
color = f"#{color[1]*2}{color[2]*2}{color[3]*2}"
|
||||
color = color.lstrip("#")
|
||||
|
||||
elif len(color) != 7:
|
||||
if len(color) == 3:
|
||||
color = f"{color[1]*2}{color[2]*2}{color[3]*2}"
|
||||
|
||||
elif len(color) != 6:
|
||||
raise TypeError("Color must be 3 or 6 character hex string")
|
||||
|
||||
return str.__new__(cls, color)
|
||||
return str.__new__(cls, "#" + color)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Color({self})"
|
||||
return f"Color('{self}')"
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_rgb(cls: type[Self], red: int, green: int, blue: int) -> Self:
|
||||
"""
|
||||
Create a new ``Color`` object from red, green, and blue values. The values must be
|
||||
between ``0`` and ``255``
|
||||
|
||||
:param red: Red color value
|
||||
:param green: Green color value
|
||||
:param blue: Blue color value
|
||||
"""
|
||||
|
||||
values = [red, green, blue]
|
||||
|
||||
for value in values:
|
||||
|
@ -140,6 +70,10 @@ class Color(str):
|
|||
|
||||
@classmethod
|
||||
def from_hsl(cls: type[Self], hue: int, saturation: int, luminance: int) -> Self:
|
||||
"""
|
||||
Create a new ``Color`` object from hue, saturation, and luminance values
|
||||
"""
|
||||
|
||||
hsl_values: list[int | float] = [hue, saturation, luminance]
|
||||
|
||||
for idx, value in enumerate(hsl_values):
|
||||
|
@ -155,23 +89,14 @@ class Color(str):
|
|||
|
||||
|
||||
@classmethod
|
||||
def new_pride_palette(cls: type[Self], flag: str) -> Iterable[Self]:
|
||||
"Returns a `tuple` of `Color` objects which represents a pride flag color palette"
|
||||
def new_pride_palette(cls: type[Self], flag: str) -> tuple[Self, ...]:
|
||||
"Returns multiple ``Color`` objects which represents a pride flag color palette"
|
||||
|
||||
if flag == "ace":
|
||||
flag = "asexual"
|
||||
|
||||
elif flag == "aro":
|
||||
flag = "aromantic"
|
||||
|
||||
elif flag in ["enby", "nonbinary"]:
|
||||
flag = "non-binary"
|
||||
|
||||
return tuple(cls(color) for color in PRIDE_COLORS[flag])
|
||||
return tuple(cls(color) for color in PridePalette.parse(flag.replace("-", "")))
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_multi(multiplier: int) -> float:
|
||||
def _parse_multi(multiplier: int) -> float:
|
||||
if multiplier >= 100:
|
||||
return 1
|
||||
|
||||
|
@ -183,11 +108,15 @@ class Color(str):
|
|||
|
||||
@cached_property
|
||||
def rgb(self) -> tuple[int, int, int]:
|
||||
"Get the color as a tuple of red, green, and blue levels"
|
||||
|
||||
return (self.red, self.green, self.blue)
|
||||
|
||||
|
||||
@cached_property
|
||||
def hsl(self) -> tuple[int, int, int]:
|
||||
"Get the color as a tuple of hue, saturation, and luminance levels"
|
||||
|
||||
rgb = [value / 255 for value in self.rgb]
|
||||
values = [int(value * 255) for value in rgb_to_hls(*rgb)]
|
||||
return (values[0], values[2], values[1])
|
||||
|
@ -195,48 +124,70 @@ class Color(str):
|
|||
|
||||
@cached_property
|
||||
def red(self) -> int:
|
||||
"Get the red color level"
|
||||
|
||||
return int(self[1:3], 16)
|
||||
|
||||
|
||||
@cached_property
|
||||
def green(self) -> int:
|
||||
"Get the green color level"
|
||||
|
||||
return int(self[3:5], 16)
|
||||
|
||||
|
||||
@cached_property
|
||||
def blue(self) -> int:
|
||||
"Get the blue color level"
|
||||
|
||||
return int(self[5:7], 16)
|
||||
|
||||
|
||||
@cached_property
|
||||
def hue(self) -> int:
|
||||
"Get the hue level"
|
||||
|
||||
return self.hsl[0]
|
||||
|
||||
|
||||
@cached_property
|
||||
def saturation(self) -> int:
|
||||
"Get the saturation level"
|
||||
|
||||
return self.hsl[1]
|
||||
|
||||
|
||||
@cached_property
|
||||
def lightness(self) -> int:
|
||||
def luminance(self) -> int:
|
||||
"Get the luminance level"
|
||||
|
||||
return self.hsl[2]
|
||||
|
||||
|
||||
def alter(self, action: str, multiplier: int) -> Self:
|
||||
def alter(self,
|
||||
action: Literal["lighten", "darken", "saturate", "desaturate"],
|
||||
multiplier: int) -> Self:
|
||||
"""
|
||||
Change the lightness or saturation of the color
|
||||
|
||||
:param action: Modification action to take on the color
|
||||
:param multiplier: Amount to multiply by for the effect. Any value outside
|
||||
0 - 100 will be changed to the nearest valid value.
|
||||
"""
|
||||
|
||||
hue, saturation, luminance = self.hsl
|
||||
|
||||
if action == "lighten":
|
||||
luminance += int((255 - luminance) * Color.parse_multi(multiplier))
|
||||
luminance += int((255 - luminance) * Color._parse_multi(multiplier))
|
||||
|
||||
elif action == "darken":
|
||||
luminance -= int(luminance * Color.parse_multi(multiplier))
|
||||
luminance -= int(luminance * Color._parse_multi(multiplier))
|
||||
|
||||
elif action == "saturate":
|
||||
saturation += int((255 - saturation) * Color.parse_multi(multiplier))
|
||||
saturation += int((255 - saturation) * Color._parse_multi(multiplier))
|
||||
|
||||
elif action == "desaturate":
|
||||
saturation -= int(saturation * Color.parse_multi(multiplier))
|
||||
saturation -= int(saturation * Color._parse_multi(multiplier))
|
||||
|
||||
else:
|
||||
raise ValueError(f"Invalid action: {action}")
|
||||
|
@ -245,22 +196,57 @@ class Color(str):
|
|||
|
||||
|
||||
def lighten(self, multiplier: int) -> Self:
|
||||
"""
|
||||
Alias of ``Color.alter('lighten', multiplier)``
|
||||
|
||||
:param multiplier: Amount to multiply by for the effect. Any value outside
|
||||
0 - 100 will be changed to the nearest valid value.
|
||||
"""
|
||||
|
||||
return self.alter("lighten", multiplier)
|
||||
|
||||
|
||||
def darken(self, multiplier: int) -> Self:
|
||||
"""
|
||||
Alias of ``Color.alter('darken', multiplier)``
|
||||
|
||||
:param multiplier: Amount to multiply by for the effect. Any value outside
|
||||
0 - 100 will be changed to the nearest valid value.
|
||||
"""
|
||||
|
||||
return self.alter("darken", multiplier)
|
||||
|
||||
|
||||
def saturate(self, multiplier: int) -> Self:
|
||||
"""
|
||||
Alias of ``Color.alter('saturate', multiplier)``
|
||||
|
||||
:param multiplier: Amount to multiply by for the effect. Any value outside
|
||||
0 - 100 will be changed to the nearest valid value.
|
||||
"""
|
||||
|
||||
return self.alter("saturate", multiplier)
|
||||
|
||||
|
||||
def desaturate(self, multiplier: int) -> Self:
|
||||
"""
|
||||
Alias of ``Color.alter('desaturate', multiplier)``
|
||||
|
||||
:param multiplier: Amount to multiply by for the effect. Any value outside
|
||||
0 - 100 will be changed to the nearest valid value.
|
||||
"""
|
||||
|
||||
return self.alter("desaturate", multiplier)
|
||||
|
||||
|
||||
def rgba(self, opacity: int) -> str:
|
||||
"""
|
||||
Return the color as a CSS ``rgba`` value
|
||||
|
||||
:param opacity: Opacity level to apply to the color. Any value outside
|
||||
0 - 100 will be changed to the nearest valid value.
|
||||
"""
|
||||
|
||||
if 0 < opacity > 100:
|
||||
raise ValueError("Opacity must anywhere from 0 to 100")
|
||||
|
||||
|
@ -270,6 +256,9 @@ class Color(str):
|
|||
|
||||
|
||||
class Cookie:
|
||||
"Represents an HTTP cookie"
|
||||
|
||||
|
||||
def __init__(self,
|
||||
key: str,
|
||||
value: str | None,
|
||||
|
@ -280,23 +269,60 @@ class Cookie:
|
|||
secure: bool = False,
|
||||
http_only: bool = False,
|
||||
same_site: Literal["lax", "strict", "none"] | None = "lax") -> None:
|
||||
"""
|
||||
Create a new cookie
|
||||
|
||||
:param key: Name of the cookie
|
||||
:param value: Value of the cookie
|
||||
:param max_age: Maximum amount of time for the cookie to be valid
|
||||
:param expires: Date the cookie expires
|
||||
:param path: Path base the cookie should be used on
|
||||
:param domain: Domain the cookie should be used on
|
||||
:param secure: Ensure the cookie is only used with ``HTTPS`` connections
|
||||
:param http_only: Prevent on-page javascript from accessing the cookie
|
||||
:param same_site: Make sure the cookie stays on the same website
|
||||
"""
|
||||
|
||||
if isinstance(max_age, int):
|
||||
max_age = timedelta(seconds = min(0, max_age))
|
||||
|
||||
self.key: str = key
|
||||
"Name of the cookie"
|
||||
|
||||
self.value: str | None = value or None
|
||||
"Value of the cookie"
|
||||
|
||||
self.max_age: timedelta | None = max_age
|
||||
"Maximum amount of time for the cookie to be valid"
|
||||
|
||||
self.expires: HttpDate | None = HttpDate.parse(expires) if expires is not None else None
|
||||
"Date the cookie expires"
|
||||
|
||||
self.path: str = path
|
||||
"Path base the cookie should be used on"
|
||||
|
||||
self.domain: str | None = domain
|
||||
self.same_site: Literal["lax", "strict", "none"] = same_site or "none"
|
||||
"Domain the cookie should be used on"
|
||||
|
||||
self.secure: bool = secure
|
||||
"Ensure the cookie is only used with ``HTTPS`` connections"
|
||||
|
||||
self.http_only: bool = False
|
||||
"Prevent on-page javascript from accessing the cookie"
|
||||
|
||||
self.same_site: Literal["lax", "strict", "none"] = same_site or "none"
|
||||
"Make sure the cookie stays on the same website"
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[Self], data: dict[str, Any]) -> Self:
|
||||
"""
|
||||
Create a new cookie from a :class:`dict`. The keys can be HTTP cookie keys or
|
||||
``Cookie`` property names.
|
||||
|
||||
:param data: Dictionary of cookie properties
|
||||
"""
|
||||
|
||||
same_site = data.get("same_site", data.get("samesite", "none")).lower()
|
||||
|
||||
if same_site not in {"lax", "strict", "none"}:
|
||||
|
@ -319,6 +345,12 @@ class Cookie:
|
|||
|
||||
@classmethod
|
||||
def parse(cls: type[Self], raw_data: str) -> Self:
|
||||
"""
|
||||
Read a raw cookie string and create a new ``Cookie`` object
|
||||
|
||||
:param raw_data: Raw cookie data
|
||||
"""
|
||||
|
||||
lines = tuple(line.strip() for line in raw_data.split(";"))
|
||||
ckey, _, cvalue = lines[0].partition("=")
|
||||
|
||||
|
@ -338,14 +370,20 @@ class Cookie:
|
|||
|
||||
|
||||
def copy(self) -> Self:
|
||||
"Create a copy of the cookie"
|
||||
|
||||
return type(self).from_dict(self.to_dict())
|
||||
|
||||
|
||||
def set_deleted(self) -> None:
|
||||
"Set the expires property to a date in the past"
|
||||
|
||||
self.expires = HttpDate.parse("Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"Return the cookie as a dict"
|
||||
|
||||
keys = (
|
||||
"key", "value", "max_age", "expires", "path",
|
||||
"domain", "same_site", "secure", "http_only"
|
||||
|
@ -355,6 +393,8 @@ class Cookie:
|
|||
|
||||
|
||||
def to_string(self) -> str:
|
||||
"Convert the cookie to a raw cookie string"
|
||||
|
||||
values = [
|
||||
f"{self.key}={self.value or ''}",
|
||||
f"path={self.path}",
|
||||
|
@ -380,7 +420,16 @@ class Cookie:
|
|||
|
||||
|
||||
class StateProxy(MutableMapping[str, Any]):
|
||||
"Proxy object for the ASGI state value"
|
||||
|
||||
|
||||
def __init__(self, state: dict[str, str]) -> None:
|
||||
"""
|
||||
Create a new state proxy object
|
||||
|
||||
:param state: A dict to proxy (usually the ASGI state)
|
||||
"""
|
||||
|
||||
self._state: dict[str, str] = state
|
||||
|
||||
|
||||
|
@ -430,29 +479,55 @@ class StateProxy(MutableMapping[str, Any]):
|
|||
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Get a value or return the default if it does not exist
|
||||
|
||||
:param key: Name of the value to get
|
||||
:param default: Value to return if the key does not exist
|
||||
"""
|
||||
|
||||
return self._state.get(key, default)
|
||||
|
||||
|
||||
def items(self) -> ItemsView[str, Any]:
|
||||
"Return all state items as key/value pairs"
|
||||
|
||||
return self._state.items()
|
||||
|
||||
|
||||
def keys(self) -> KeysView[str]:
|
||||
"Return all keys in the state"
|
||||
|
||||
return self._state.keys()
|
||||
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Set a state value
|
||||
|
||||
:param key: Name of the value to set
|
||||
:param value: Data to set
|
||||
"""
|
||||
|
||||
self._state[key] = value
|
||||
|
||||
|
||||
def values(self) -> ValuesView[Any]:
|
||||
"Return all values in the state"
|
||||
|
||||
return self._state.values()
|
||||
|
||||
|
||||
class Stream:
|
||||
"Contains the reader and writer functions. Can be used as an iterator for fetching data chunks."
|
||||
|
||||
def __init__(self, reader: ReaderFunction, writer: WriterFunction) -> None:
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.reader: ReaderFunction = reader
|
||||
"Function for reading data from a client"
|
||||
|
||||
self.writer: WriterFunction = writer
|
||||
"Function for writing data to a client"
|
||||
|
||||
self._sent_headers: bool = False
|
||||
self._sent_body: bool = False
|
||||
self.__next: bool = True
|
||||
|
@ -474,10 +549,14 @@ class Stream:
|
|||
|
||||
|
||||
async def close(self) -> None:
|
||||
"Disconnect from the client"
|
||||
|
||||
await self.writer({"type": "http.disconnect"})
|
||||
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"Read the whole request body"
|
||||
|
||||
body = b""
|
||||
|
||||
while True:
|
||||
|
@ -491,11 +570,20 @@ class Stream:
|
|||
|
||||
|
||||
async def read_chunk(self) -> tuple[bytes, bool]:
|
||||
"Read a chunk of data from the request and return a tuple of (``body chunk``, ``more body``)"
|
||||
|
||||
data = await self.reader()
|
||||
return data.get("body", b""), data.get("more_body", False)
|
||||
|
||||
|
||||
async def write_headers(self, status: int, headers: Sequence[tuple[bytes, bytes]]) -> None:
|
||||
"""
|
||||
Write the response headers. If this was already called, nothing happens.
|
||||
|
||||
:param status: HTTP status to respond with
|
||||
:param headers: A sequence of HTTP header values to be sent with the response
|
||||
"""
|
||||
|
||||
if self._sent_headers:
|
||||
return
|
||||
|
||||
|
@ -505,8 +593,20 @@ class Stream:
|
|||
"headers": headers
|
||||
})
|
||||
|
||||
self._sent_headers = True
|
||||
|
||||
|
||||
async def write_body(self, data: bytes, eof: bool = False) -> None:
|
||||
"""
|
||||
Send body data to the client. If an eof was already sent, this does nothing.
|
||||
|
||||
:param data: Raw data to send to the client
|
||||
:param eof: Send an ``EOF`` to indicate the response is done.
|
||||
"""
|
||||
|
||||
if not self._sent_headers:
|
||||
raise RuntimeError("Response headers have not been sent yet")
|
||||
|
||||
if self._sent_body:
|
||||
return
|
||||
|
||||
|
@ -515,3 +615,6 @@ class Stream:
|
|||
"body": data,
|
||||
"more_body": eof
|
||||
})
|
||||
|
||||
if eof:
|
||||
self._sent_body = True
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import os
|
||||
|
||||
from aputils import HttpDate, JsonBase
|
||||
from blib import convert_to_bytes
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime, timedelta
|
||||
from mimetypes import guess_type
|
||||
|
@ -14,7 +15,7 @@ from urllib.parse import quote
|
|||
|
||||
from .enums import HttpStatus
|
||||
from .error import HttpError
|
||||
from .misc import Cookie, Stream, convert_to_bytes
|
||||
from .misc import Cookie, Stream
|
||||
from .request import Request
|
||||
|
||||
|
||||
|
@ -268,7 +269,7 @@ class TemplateResponse(Response):
|
|||
status: HttpStatus | int = HttpStatus.Ok,
|
||||
mimetype: str | None = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
pretty_print: bool = False) -> None:
|
||||
pprint: bool = False) -> None:
|
||||
"""
|
||||
Create a new ``TemplateResponse`` object
|
||||
|
||||
|
@ -283,12 +284,17 @@ class TemplateResponse(Response):
|
|||
Response.__init__(self, status, b"", mimetype or self.detect_mimetype(name), headers)
|
||||
|
||||
self.name: str = name
|
||||
self.pretty_print: bool = pretty_print
|
||||
self.pprint: bool = pprint
|
||||
self.context: dict[str, Any] = context or {}
|
||||
|
||||
|
||||
def detect_mimetype(self, path: str) -> str:
|
||||
"Return the mimetype of the template based on the file extension"
|
||||
@staticmethod
|
||||
def detect_mimetype(path: str) -> str:
|
||||
"""
|
||||
Return the mimetype of the template based on the file extension
|
||||
|
||||
:param path: Path to detect the mimetype of
|
||||
"""
|
||||
|
||||
_, ext = os.path.splitext(path)
|
||||
|
||||
|
@ -306,7 +312,7 @@ class TemplateResponse(Response):
|
|||
"request": request,
|
||||
**self.context
|
||||
}
|
||||
text = request.app.template.render(self.name, context, self.pretty_print)
|
||||
text = request.app.template.render(self.name, context, self.pprint)
|
||||
self.body = text.encode("utf-8")
|
||||
|
||||
await Response.send(self, stream, request)
|
||||
|
|
156
basgi/signal.py
156
basgi/signal.py
|
@ -1,156 +0,0 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import traceback
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Self
|
||||
|
||||
from .misc import is_loop_running
|
||||
|
||||
|
||||
SignalCallback = Callable[..., Awaitable[bool | None]]
|
||||
MainSignalCallback = Callable[..., Awaitable[None]]
|
||||
|
||||
|
||||
class Signal(list[SignalCallback]):
|
||||
"Allows a series of callbacks to get called via async. Use as a decorator for the base function."
|
||||
|
||||
def __init__(self, timeout: float | int = 5.0):
|
||||
"""
|
||||
:param timeout: Time in seconds to wait before cancelling the callback
|
||||
"""
|
||||
|
||||
self.timeout: float | int = timeout
|
||||
self.callback: MainSignalCallback | None = None
|
||||
self.object: Any = None
|
||||
|
||||
|
||||
def __get__(self, obj: Any, objtype: type[Any]) -> Self:
|
||||
if obj and not self.object:
|
||||
self.object = obj
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def __call__(self, callback: MainSignalCallback) -> Self:
|
||||
if not inspect.iscoroutinefunction(callback):
|
||||
raise RuntimeError(f"Not a coroutine: {callback.__name__}")
|
||||
|
||||
self.callback = callback
|
||||
|
||||
self.__doc__ = callback.__doc__
|
||||
self.__annotations__ = callback.__annotations__
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def append(self, value: SignalCallback) -> None:
|
||||
"""
|
||||
Add a callback
|
||||
|
||||
:param value: Callback
|
||||
"""
|
||||
|
||||
self.connect(value)
|
||||
|
||||
|
||||
def remove(self, value: SignalCallback) -> None:
|
||||
"""
|
||||
Remove a callback
|
||||
|
||||
:param value: Callback
|
||||
"""
|
||||
|
||||
self.disconnect(value)
|
||||
|
||||
|
||||
def emit(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Call all of the callbacks in the order they were added as well as the associated
|
||||
function.
|
||||
|
||||
If any callback returns `True`, all other callbacks get skipped.
|
||||
|
||||
:param args: Positional arguments to pass to all of the callbacks
|
||||
:param kwargs: Keyword arguments to pass to all of the callbacks
|
||||
"""
|
||||
|
||||
if not is_loop_running():
|
||||
raise RuntimeError("Event loop is not running")
|
||||
|
||||
asyncio.create_task(self.handle_emit(*args, **kwargs))
|
||||
|
||||
|
||||
def connect(self, callback: SignalCallback) -> SignalCallback:
|
||||
"""
|
||||
Add a function to the list of callbacks. Can be used as a decorator.
|
||||
|
||||
:param callback: A callable or coroutine
|
||||
"""
|
||||
|
||||
if callback not in self:
|
||||
list.append(self, callback)
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def disconnect(self, callback: SignalCallback) -> None:
|
||||
"""
|
||||
Remove a function from the list of callbacks
|
||||
|
||||
:param callback: A callable or coroutine
|
||||
"""
|
||||
|
||||
if not self.callback:
|
||||
# oh boy something really goofed
|
||||
return
|
||||
|
||||
try:
|
||||
list.remove(self, callback)
|
||||
|
||||
except ValueError:
|
||||
cbname = callback.__name__
|
||||
signame = self.callback.__name__
|
||||
print(f"WARNING: '{cbname}' was not connted to signal '{signame}'")
|
||||
|
||||
|
||||
async def handle_emit(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
This gets called by :meth:`Signal.emit` as an :class:`asyncio.Task`.
|
||||
|
||||
:param args: Positional arguments to pass to all of the callbacks
|
||||
:param kwargs: Keyword arguments to pass to all of the callbacks
|
||||
"""
|
||||
|
||||
if not self.callback:
|
||||
# oh boy something really goofed
|
||||
return
|
||||
|
||||
for callback in self:
|
||||
if await self.handle_callback(callback, *args, **kwargs):
|
||||
break
|
||||
|
||||
await self.handle_callback(self.callback, *args, **kwargs)
|
||||
|
||||
|
||||
async def handle_callback(self,
|
||||
callback: Callable[..., Awaitable[bool | None]],
|
||||
*args: Any,
|
||||
**kwargs: Any) -> bool | None:
|
||||
|
||||
try:
|
||||
# asyncio.timeout and asyncio.wait_for complain, so need to find a new way
|
||||
# async with asyncio.timeout(self.timeout):
|
||||
if args and args[0] == self.object:
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
else:
|
||||
return await callback(self.object, *args, **kwargs)
|
||||
|
||||
except TimeoutError:
|
||||
print(f"Callback '{callback.__name__}' timed out")
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return True
|
|
@ -48,7 +48,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|||
intersphinx_mapping = {
|
||||
"python": (f"https://docs.python.org/{pyversion}", None),
|
||||
"aputils": ("https://docs.barkshark.xyz/aputils", None),
|
||||
"bsql": ("https://docs.barkshark.xyz/barkshark-sql", None),
|
||||
"blib": ("https://docs.barkshark.xyz/blib", None),
|
||||
"gemi": ("https://docs.barkshark.xyz/gemi", None),
|
||||
"hamlish-jinja": ("https://docs.barkshark.xyz/hamlish-jinja", None),
|
||||
"jinja2": ("https://jinja.palletsprojects.com/en/3.1.x/", None)
|
||||
|
|
|
@ -5,10 +5,6 @@ Application
|
|||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: basgi.Signal
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: basgi.Stream
|
||||
.. autoclass:: basgi.StaticHandler
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -7,6 +7,12 @@ Enums
|
|||
:undoc-members:
|
||||
:exclude-members: __new__
|
||||
|
||||
.. autoclass:: basgi.PridePalette
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:undoc-members:
|
||||
:exclude-members: __new__
|
||||
|
||||
.. autoclass:: basgi.SassOutputStyle
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -7,8 +7,6 @@ Application
|
|||
|
||||
:class:`basgi.Application`
|
||||
|
||||
:class:`basgi.Signal`
|
||||
|
||||
:class:`basgi.Stream`
|
||||
|
||||
|
||||
|
|
|
@ -2,10 +2,17 @@ Messages
|
|||
========
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. autoclass:: basgi.Request
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
.. autoclass:: basgi.Response
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
@ -17,3 +24,11 @@ Messages
|
|||
.. autoclass:: basgi.TemplateResponse
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Helpers
|
||||
-------
|
||||
|
||||
.. autoclass:: basgi.Cookie
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
24
docs/src/api/misc.rst
Normal file
24
docs/src/api/misc.rst
Normal file
|
@ -0,0 +1,24 @@
|
|||
Misc
|
||||
====
|
||||
|
||||
.. autoclass:: basgi.Color
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: basgi.SassExtension
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:exclude-members: __init__
|
||||
|
||||
.. autoclass:: basgi.StateProxy
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: basgi.Stream
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:exclude-members: __init__
|
||||
|
||||
.. autoclass:: basgi.Template
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -1,4 +0,0 @@
|
|||
Signal
|
||||
======
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ subtrees:
|
|||
- file: src/api/runners.rst
|
||||
- file: src/api/messages.rst
|
||||
- file: src/api/router.rst
|
||||
- file: src/api/template.rst
|
||||
- file: src/api/misc.rst
|
||||
- file: src/api/enums.rst
|
||||
- file: src/api/exceptions.rst
|
||||
- url: https://git.barkshark.xyz/barkshark/basgi
|
||||
|
|
|
@ -36,6 +36,7 @@ classifiers = [
|
|||
requires-python = ">= 3.11"
|
||||
dependencies = [
|
||||
"activitypub-utils == 0.2.3",
|
||||
"barkshark-lib == 0.1.1",
|
||||
"gemi-python == 0.1.1",
|
||||
"http-router == 4.1.2",
|
||||
"httpx[http2] == 0.27.0",
|
||||
|
@ -98,6 +99,5 @@ disallow_untyped_decorators = true
|
|||
warn_redundant_casts = true
|
||||
warn_unreachable = true
|
||||
warn_unused_ignores = true
|
||||
follow_imports = "silent"
|
||||
strict = true
|
||||
implicit_reexport = true
|
||||
|
|
Loading…
Reference in a new issue