diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..49f7b87b
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 00000000..ec186820
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://scaleway.steffo.eu:5432/royalnet
+
+
+
\ No newline at end of file
diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py
index 61fb40a7..3919c3c7 100644
--- a/royalnet/constellation/constellation.py
+++ b/royalnet/constellation/constellation.py
@@ -7,6 +7,7 @@ import royalnet.herald as rh
import royalnet.utils as ru
import royalnet.commands as rc
from .star import PageStar, ExceptionStar
+from ..utils import init_logging
try:
import uvicorn
@@ -51,7 +52,8 @@ class Constellation:
herald_cfg: Dict[str, Any],
packs_cfg: Dict[str, Any],
constellation_cfg: Dict[str, Any],
- **_):
+ logging_cfg: Dict[str, Any]
+ ):
if Starlette is None:
raise ImportError("`constellation` extra is not installed")
@@ -87,9 +89,19 @@ class Constellation:
self.alchemy = ra.Alchemy(alchemy_cfg["database_url"], tables)
log.info(f"Alchemy: {self.alchemy}")
+ # Logging
+ self._logging_cfg: Dict[str, Any] = logging_cfg
+ """The logging config for the :class:`Constellation` is stored to initialize the logger when the first page is
+ requested, as disabling the :mod:`uvicorn` logging also disables all logging in the process in general."""
+
# Herald
self.herald: Optional[rh.Link] = None
- """The :class:`Link` object connecting the :class:`Constellation` to the rest of the herald network."""
+ """The :class:`Link` object connecting the :class:`Constellation` to the rest of the herald network.
+ As is the case with the logging module, it will be started on the first request received by the
+ :class:`Constellation`, as the event loop won't be available before that."""
+
+ self._herald_cfg: Dict[str, Any] = herald_cfg
+ """The herald config for the :class:`Constellation` is stored to initialize the :class:`rh.Herald` later."""
self.herald_task: Optional[aio.Task] = None
"""A reference to the :class:`aio.Task` that runs the :class:`rh.Link`."""
@@ -120,8 +132,7 @@ class Constellation:
elif not herald_cfg["enabled"]:
log.info("Herald: disabled")
else:
- self.init_herald(herald_cfg)
- log.info(f"Herald: enabled")
+ log.info(f"Herald: will be enabled on first request")
# Register PageStars and ExceptionStars
for pack_name in packs:
@@ -151,6 +162,11 @@ class Constellation:
self.port: int = constellation_cfg["port"]
"""The port on which the :class:`Constellation` will listen for connection on."""
+ self.loop: Optional[aio.AbstractEventLoop] = None
+ """The event loop of the :class:`Constellation`.
+
+ Because of how :mod:`uvicorn` runs, it will stay :const:`None` until the first page is requested."""
+
# TODO: is this a good idea?
def interface_factory(self) -> Type[rc.CommandInterface]:
"""Create the :class:`rc.CommandInterface` class for the :class:`Constellation`."""
@@ -241,29 +257,52 @@ class Constellation:
log.debug(f"Registering: {SelectedEvent.__qualname__} -> {SelectedEvent.name}")
self.events[SelectedEvent.name] = event
+ def _first_page_check(self):
+ if self.loop is None:
+ self.loop = aio.get_running_loop()
+ self.init_herald(self._herald_cfg)
+ self.loop.create_task(self.herald.run())
+ init_logging(self._logging_cfg)
+
+ def _page_star_wrapper(self, page_star: PageStar):
+ async def f(request):
+ self._first_page_check()
+ log.info(f"Running {page_star}")
+ return await page_star.page(request)
+
+ return page_star.path, f, page_star.methods
+
+ def _exc_star_wrapper(self, exc_star: ExceptionStar):
+ async def f(request):
+ self._first_page_check()
+ log.info(f"Running {exc_star}")
+ return await exc_star.page(request)
+
+ return exc_star.error, f
+
def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]):
for SelectedPageStar in page_stars:
log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}")
try:
- page_star_instance = SelectedPageStar(constellation=self, config=pack_cfg)
+ page_star_instance = SelectedPageStar(interface=self.Interface(pack_cfg))
except Exception as e:
log.error(f"Skipping: "
f"{SelectedPageStar.__qualname__} - {e.__class__.__qualname__} in the initialization.")
ru.sentry_exc(e)
continue
- self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods)
+ self.starlette.add_route(*self._page_star_wrapper(page_star_instance))
def register_exc_stars(self, exc_stars: List[Type[ExceptionStar]], pack_cfg: Dict[str, Any]):
- for SelectedPageStar in exc_stars:
- log.debug(f"Registering: {SelectedPageStar.error} -> {SelectedPageStar.__qualname__}")
+ for SelectedExcStar in exc_stars:
+ log.debug(f"Registering: {SelectedExcStar.error} -> {SelectedExcStar.__qualname__}")
try:
- page_star_instance = SelectedPageStar(constellation=self, config=pack_cfg)
+ exc_star_instance = SelectedExcStar(interface=self.Interface(pack_cfg))
except Exception as e:
log.error(f"Skipping: "
- f"{SelectedPageStar.__qualname__} - {e.__class__.__qualname__} in the initialization.")
+ f"{SelectedExcStar.__qualname__} - {e.__class__.__qualname__} in the initialization.")
ru.sentry_exc(e)
continue
- self.starlette.add_exception_handler(page_star_instance.error, page_star_instance.page)
+ self.starlette.add_exception_handler(*self._exc_star_wrapper(exc_star_instance))
def run_blocking(self):
log.info(f"Running Constellation on https://{self.address}:{self.port}/...")
@@ -300,7 +339,8 @@ class Constellation:
constellation = cls(alchemy_cfg=alchemy_cfg,
herald_cfg=herald_cfg,
packs_cfg=packs_cfg,
- constellation_cfg=constellation_cfg)
+ constellation_cfg=constellation_cfg,
+ logging_cfg=logging_cfg)
# Run the server
constellation.run_blocking()
diff --git a/royalnet/constellation/star.py b/royalnet/constellation/star.py
index 6824e57f..657968dc 100644
--- a/royalnet/constellation/star.py
+++ b/royalnet/constellation/star.py
@@ -1,6 +1,7 @@
from typing import *
from starlette.requests import Request
from starlette.responses import Response
+from royalnet.commands import CommandInterface
if TYPE_CHECKING:
from .constellation import Constellation
@@ -10,9 +11,8 @@ class Star:
"""A Star is a class representing a part of the website.
It shouldn't be used directly: please use :class:`PageStar` and :class:`ExceptionStar` instead!"""
- def __init__(self, config: Dict[str, Any], constellation: "Constellation"):
- self.config: Dict[str, Any] = config
- self.constellation: "Constellation" = constellation
+ def __init__(self, interface: CommandInterface):
+ self.interface: CommandInterface = interface
async def page(self, request: Request) -> Response:
"""The function generating the :class:`~starlette.Response` to a web :class:`~starlette.Request`.
@@ -20,21 +20,31 @@ class Star:
If it raises an error, the corresponding :class:`ExceptionStar` will be used to handle the request instead."""
raise NotImplementedError()
+ @property
+ def constellation(self) -> "Constellation":
+ """A shortcut for the :class:`Constellation`."""
+ return self.interface.constellation
+
@property
def alchemy(self):
"""A shortcut for the :class:`~royalnet.alchemy.Alchemy` of the :class:`Constellation`."""
- return self.constellation.alchemy
+ return self.interface.constellation.alchemy
# noinspection PyPep8Naming
@property
def Session(self):
"""A shortcut for the :class:`~royalnet.alchemy.Alchemy` :class:`Session` of the :class:`Constellation`."""
- return self.constellation.alchemy.Session
+ return self.interface.constellation.alchemy.Session
@property
def session_acm(self):
"""A shortcut for :func:`.alchemy.session_acm` of the :class:`Constellation`."""
- return self.constellation.alchemy.session_acm
+ return self.interface.constellation.alchemy.session_acm
+
+ @property
+ def config(self) -> Dict[str, Any]:
+ """A shortcut for the Pack configuration of the :class:`Constellation`."""
+ return self.interface.config
def __repr__(self):
return f"<{self.__class__.__qualname__}>"
diff --git a/royalnet/utils/log.py b/royalnet/utils/log.py
index ba1ab9c5..06686518 100644
--- a/royalnet/utils/log.py
+++ b/royalnet/utils/log.py
@@ -9,7 +9,32 @@ except ImportError:
l: logging.Logger = logging.getLogger(__name__)
+# From https://stackoverflow.com/a/56810619/4334568
+def reset_logging():
+ manager = logging.root.manager
+ manager.disabled = logging.NOTSET
+ for logger in manager.loggerDict.values():
+ if isinstance(logger, logging.Logger):
+ logger.setLevel(logging.NOTSET)
+ logger.propagate = True
+ logger.disabled = False
+ logger.filters.clear()
+ handlers = logger.handlers.copy()
+ for handler in handlers:
+ # Copied from `logging.shutdown`.
+ try:
+ handler.acquire()
+ handler.flush()
+ handler.close()
+ except (OSError, ValueError):
+ pass
+ finally:
+ handler.release()
+ logger.removeHandler(handler)
+
+
def init_logging(logging_cfg: Dict[str, Any]):
+ reset_logging()
loggers_cfg = logging_cfg["Loggers"]
for logger_name in loggers_cfg:
if logger_name == "root":
@@ -23,7 +48,7 @@ def init_logging(logging_cfg: Dict[str, Any]):
stream_handler.formatter = coloredlogs.ColoredFormatter(logging_cfg["log_format"], style="{")
else:
stream_handler.formatter = logging.Formatter(logging_cfg["log_format"], style="{")
- if len(logging.root.handlers) < 1:
- logging.root.addHandler(stream_handler)
+ logging.root.handlers.clear()
+ logging.root.addHandler(stream_handler)
l.debug("Logging: ready")