-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ccb2d65
commit 7924a21
Showing
11 changed files
with
308 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
middleware: | ||
- selva.web.middleware.files.StaticFilesMiddleware | ||
- selva.web.middleware.request_id.RequestIdMiddleware | ||
|
||
logging: | ||
root: info | ||
level: | ||
application: info | ||
format: console |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Lorem ipsum dolor sit amet. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
LOGGING = { | ||
"version": 1, | ||
"disable_existing_loggers": True, | ||
"formatters": { | ||
"default": { | ||
"format": "{asctime} {levelname:<8} {message}", | ||
"datefmt": "%Y-%m-%dT%H:%M:%S", | ||
"style": "{", | ||
}, | ||
"logfmt": { | ||
"()": "logfmter.Logfmter", | ||
"keys": ["timestamp", "level", "name", "event"], | ||
"mapping": { | ||
"timestamp": "asctime", | ||
"level": "levelname", | ||
"event": "message", | ||
}, | ||
"datefmt": "%Y-%m-%dT%H:%M:%S", | ||
}, | ||
}, | ||
"handlers": { | ||
"console": { | ||
"level": "INFO", | ||
"class": "logging.StreamHandler", | ||
"formatter": "default", | ||
}, | ||
}, | ||
"root": { | ||
"handlers": ["console"], | ||
"level": "INFO", | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import asyncio | ||
import datetime | ||
import functools | ||
import inspect | ||
import json | ||
import logging | ||
import string | ||
import sys | ||
import threading | ||
import traceback | ||
from collections.abc import Callable | ||
from enum import IntEnum | ||
from functools import cached_property | ||
from typing import Any, TypeAlias | ||
|
||
TRACE = 5 | ||
|
||
logging.addLevelName(TRACE, "trace") | ||
|
||
logging.getLevelNamesMapping() | ||
|
||
LogEvent: TypeAlias = str | tuple[str, list] | ||
|
||
|
||
class LogLevel(IntEnum): | ||
NOTSET = logging.NOTSET | ||
TRACE = TRACE | ||
DEBUG = logging.DEBUG | ||
INFO = logging.INFO | ||
WARNING = logging.WARN | ||
ERROR = logging.ERROR | ||
CRITICAL = logging.CRITICAL | ||
|
||
|
||
class LogRecord: | ||
def __init__( | ||
self, | ||
level: LogLevel, | ||
logger_name: str, | ||
event: LogEvent, | ||
context: dict, | ||
exception: BaseException = None, | ||
timestamp: datetime.datetime = None, | ||
): | ||
self.timestamp = timestamp or datetime.datetime.now() | ||
self.log_level = level | ||
self.log_logger = logger_name | ||
self.log_context = context | ||
self.log_event = event | ||
self.exception = exception | ||
|
||
@cached_property | ||
def event(self): | ||
return self.format_event(self.log_event, self.log_context) | ||
|
||
@staticmethod | ||
def format_event(event: LogEvent, context: dict) -> str: | ||
match event: | ||
case event if isinstance(event, str): | ||
return event.format(**context) | ||
case (event, *args) if isinstance(event, str): | ||
return event.format(*args, **context) | ||
case _: | ||
raise ValueError("Invalid event") | ||
|
||
|
||
def _normalize_name(name: str) -> str: | ||
for char in string.whitespace: | ||
name = name.replace(char, "_").strip() | ||
return name | ||
|
||
|
||
def _normalize_value(value: Any, depth=0): | ||
if depth == 0: | ||
if isinstance(value, bool) and depth == 0: | ||
return "true" if value else "false" | ||
|
||
if isinstance(value, str) and depth == 0: | ||
return json.dumps(value) | ||
|
||
if isinstance(value, (list, tuple, set, frozenset)): | ||
return json.dumps([_normalize_value(v, depth + 1) for v in value]) | ||
|
||
if isinstance(value, dict): | ||
return json.dumps( | ||
{k: _normalize_value(v, depth + 1) for k, v in value.items()} | ||
) | ||
|
||
if isinstance(value, (str, int, float, bool)): | ||
return value | ||
|
||
return repr(value) | ||
|
||
|
||
def _formatter(record: LogRecord) -> str: | ||
message = json.dumps(record.event) | ||
context = " ".join( | ||
f"{_normalize_name(name)}={_normalize_value(value)}" | ||
for name, value in record.log_context.items() | ||
) | ||
return f"{record.timestamp.isoformat()} {record.log_level:<8} {message} {context}" | ||
|
||
|
||
class PrintHandler: | ||
def __init__(self, formatter: Callable[[LogRecord], str] = _formatter): | ||
self.formatter = formatter | ||
|
||
def __call__(self, record: LogRecord): | ||
message = self.formatter(record) | ||
print(message, file=sys.stderr) | ||
|
||
|
||
class Logger: | ||
def __init__(self): | ||
self.root_level = LogLevel.INFO | ||
self.level_spec = {} | ||
self.handler = PrintHandler() | ||
|
||
def log(self, level: LogLevel, event: LogEvent, **kwargs): | ||
# if level < self.level: | ||
# return | ||
|
||
name = inspect.currentframe().f_globals.get("__name__") | ||
record = LogRecord(level, name, event, kwargs) | ||
self.queue.put_nowait(record) | ||
# message = _formatter(record) | ||
|
||
# logger = logging.getLogger(name) | ||
# logger.log(level, message) | ||
|
||
# self.handler(record) | ||
|
||
def trace(self, event: LogEvent, **kwargs): | ||
self.log(LogLevel.TRACE, event, **kwargs) | ||
|
||
def debug(self, event: LogEvent, **kwargs): | ||
self.log(LogLevel.DEBUG, event, **kwargs) | ||
|
||
def info(self, event: LogEvent, **kwargs): | ||
self.log(LogLevel.INFO, event, **kwargs) | ||
|
||
def warning(self, event: LogEvent, **kwargs): | ||
self.log(LogLevel.WARNING, event, **kwargs) | ||
|
||
def error(self, event: LogEvent, **kwargs): | ||
self.log(LogLevel.ERROR, event, **kwargs) | ||
|
||
def exception(self, event: LogEvent | BaseException, *, exception=None, **kwargs): | ||
if isinstance(event, BaseException): | ||
exception = event | ||
event = str(event) | ||
|
||
if exception or (exception := sys.exception()): | ||
kwargs["exception"] = "".join(traceback.format_exception(exception)).rstrip( | ||
"\n" | ||
) | ||
|
||
self.log(LogLevel.ERROR, event, **kwargs) | ||
|
||
def critical(self, event: LogEvent, **kwargs): | ||
self.log(LogLevel.CRITICAL, event, **kwargs) | ||
|
||
# @overload | ||
# def log(self, record: LogRecord, call_level=0): | ||
# if call_level: | ||
# frame = inspect.currentframe() | ||
# while call_level: | ||
# frame = frame.f_back | ||
# call_level -= 1 | ||
# record.name = frame.f_globals.get("__name__") | ||
# | ||
# self.handler(record) | ||
|
||
|
||
if __name__ == "__main__": | ||
logger = Logger() | ||
logger.info("logger") | ||
import time | ||
|
||
time.sleep(2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
from pathlib import Path | ||
from typing import Annotated | ||
|
||
from asgikit.requests import Request | ||
from asgikit.responses import respond_file | ||
|
||
from selva.configuration import Settings | ||
from selva.di import Inject | ||
from selva.web.middleware import CallNext, Middleware | ||
from selva.web.exception import HTTPNotFoundException | ||
|
||
|
||
class StaticFilesMiddleware(Middleware): | ||
settings: Annotated[Settings, Inject] | ||
|
||
def initialize(self): | ||
self.path = self.settings.staticfiles.path | ||
self.root = Path(self.settings.staticfiles.root).resolve() | ||
self.mappings = { | ||
name.lstrip("/"): value.lstrip("/") for name, value in | ||
self.settings.staticfiles.get("mappings").items() | ||
} | ||
|
||
async def __call__( | ||
self, | ||
call_next: CallNext, | ||
request: Request, | ||
): | ||
file_to_serve = self.mappings.get(request.path.lstrip("/")) | ||
|
||
if not file_to_serve: | ||
if not request.path.startswith(self.path): | ||
await call_next(request) | ||
return | ||
|
||
file_to_serve = request.path.removeprefix(self.path) | ||
|
||
file_to_serve = file_to_serve.lstrip("/") | ||
path_to_serve = (self.root / file_to_serve).resolve() | ||
if not path_to_serve.is_relative_to(self.root) or not path_to_serve.exists() or not path_to_serve.is_file(): | ||
raise HTTPNotFoundException() | ||
|
||
await respond_file(request.response, path_to_serve) | ||
|
||
|
||
class UploadedFilesMiddleware(Middleware): | ||
settings: Annotated[Settings, Inject] | ||
|
||
def initialize(self): | ||
self.path = self.settings.uploadedfiles.path | ||
self.root = Path(self.settings.uploadedfiles.root).resolve() | ||
|
||
async def __call__( | ||
self, | ||
call_next: CallNext, | ||
request: Request, | ||
): | ||
if not request.path.startswith(self.path): | ||
await call_next(request) | ||
return | ||
|
||
file_to_serve = request.path.removeprefix(self.path).lstrip("/") | ||
path_to_serve = (self.root / file_to_serve).resolve() | ||
if not path_to_serve.is_relative_to(self.root) or not path_to_serve.exists() or not path_to_serve.is_file(): | ||
raise HTTPNotFoundException() | ||
|
||
await respond_file(request.response, path_to_serve) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters