mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
Document and refactor constellation
This commit is contained in:
parent
e1b76fed6b
commit
caf16ddf50
10 changed files with 141 additions and 81 deletions
|
@ -3,5 +3,8 @@
|
|||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="MacroExpansionManager">
|
||||
<option name="directoryName" value="cqweyjJr" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
|
||||
</project>
|
2
TODO.md
2
TODO.md
|
@ -6,6 +6,6 @@
|
|||
- [ ] interfaces
|
||||
- [ ] packs (almost)
|
||||
- [ ] utils
|
||||
- [ ] web
|
||||
- [x] constellation
|
||||
- [ ] main
|
||||
- [ ] dependencies
|
|
@ -33,6 +33,8 @@ sqlalchemy = {version="^1.3.10", optional=true}
|
|||
psycopg2 = {version="^2.8.4", optional=true} # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source
|
||||
psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
|
||||
|
||||
starlette = {version="0.12.13", optional=true}
|
||||
|
||||
# Optional dependencies
|
||||
[tool.poetry.extras]
|
||||
telegram = ["python_telegram_bot"]
|
||||
|
@ -40,6 +42,7 @@ discord = ["discord_py", "pynacl"]
|
|||
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
||||
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
||||
bard = ["ffmpeg_python", "youtube_dl"]
|
||||
constellation = ["starlette"]
|
||||
|
||||
# Development dependencies
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
|
5
royalnet/constellation/README.md
Normal file
5
royalnet/constellation/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# `royalnet.constellation`
|
||||
|
||||
The part of `royalnet` that handles the webserver and webpages.
|
||||
|
||||
It uses many features of [`starlette`](https://www.starlette.io).
|
|
@ -1,11 +1,15 @@
|
|||
"""The part of :mod:`royalnet` that handles the webserver and webpages.
|
||||
|
||||
It uses many features of :mod:`starlette`."""
|
||||
|
||||
from .constellation import Constellation
|
||||
from .star import Star, PageStar, ExceptionStar
|
||||
from .error import error
|
||||
from .shoot import shoot
|
||||
|
||||
__all__ = [
|
||||
"Constellation",
|
||||
"Star",
|
||||
"PageStar",
|
||||
"ExceptionStar",
|
||||
"error",
|
||||
"shoot",
|
||||
]
|
|
@ -15,6 +15,11 @@ log = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class Constellation:
|
||||
"""A Constellation is the class that represents the webserver.
|
||||
|
||||
It runs multiple :class:`Star`s, which represent the routes of the website.
|
||||
|
||||
It also handles the :class:`Alchemy` connection, and it _will_ support Herald connections too."""
|
||||
def __init__(self,
|
||||
secrets_name: str,
|
||||
database_uri: str,
|
||||
|
@ -31,35 +36,48 @@ class Constellation:
|
|||
|
||||
self.secrets_name: str = secrets_name
|
||||
|
||||
log.info("Creating starlette app...")
|
||||
log.info(f"Creating Starlette in {'Debug' if __debug__ else 'Production'} mode...")
|
||||
self.starlette = Starlette(debug=debug)
|
||||
|
||||
log.info(f"Creating alchemy with tables: {' '.join([table.__name__ for table in tables])}")
|
||||
log.info(f"Creating Alchemy with Tables: {' '.join([table.__name__ for table in tables])}")
|
||||
self.alchemy: royalnet.database.Alchemy = royalnet.database.Alchemy(database_uri=database_uri, tables=tables)
|
||||
|
||||
log.info("Registering page_stars...")
|
||||
log.info("Registering PageStars...")
|
||||
for SelectedPageStar in page_stars:
|
||||
log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}")
|
||||
try:
|
||||
page_star_instance = SelectedPageStar(constellation=self)
|
||||
except Exception as e:
|
||||
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}")
|
||||
sentry_sdk.capture_exception(e)
|
||||
continue
|
||||
log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}")
|
||||
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}!")
|
||||
raise
|
||||
self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods)
|
||||
|
||||
log.info("Registering exc_stars...")
|
||||
log.info("Registering ExceptionStars...")
|
||||
for SelectedExcStar in exc_stars:
|
||||
log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}")
|
||||
try:
|
||||
exc_star_instance = SelectedExcStar(constellation=self)
|
||||
except Exception as e:
|
||||
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}")
|
||||
sentry_sdk.capture_exception(e)
|
||||
continue
|
||||
log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}")
|
||||
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}!")
|
||||
raise
|
||||
self.starlette.add_exception_handler(exc_star_instance.error, exc_star_instance.page)
|
||||
|
||||
def _init_sentry(self):
|
||||
def get_secret(self, username: str) -> typing.Optional[str]:
|
||||
"""Get a Royalnet secret from the keyring.
|
||||
|
||||
Args:
|
||||
username: the name of the secret that should be retrieved."""
|
||||
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
|
||||
|
||||
def run_blocking(self, address: str, port: int):
|
||||
"""Blockingly run the Constellation.
|
||||
|
||||
This should be used as the target of a :class:`multiprocessing.Process`.
|
||||
|
||||
Args:
|
||||
address: The IP address this Constellation should bind to.
|
||||
port: The port this Constellation should listen for requests on."""
|
||||
# Initialize Sentry on the process
|
||||
sentry_dsn = self.get_secret("sentry")
|
||||
if sentry_dsn:
|
||||
# noinspection PyUnreachableCode
|
||||
|
@ -68,28 +86,13 @@ class Constellation:
|
|||
else:
|
||||
release = royalnet.version.semantic
|
||||
log.info(f"Sentry: enabled (Royalnet {release})")
|
||||
self.sentry = sentry_sdk.init(sentry_dsn,
|
||||
integrations=[AioHttpIntegration(),
|
||||
SqlalchemyIntegration(),
|
||||
LoggingIntegration(event_level=None)],
|
||||
release=release)
|
||||
sentry_sdk.init(sentry_dsn,
|
||||
integrations=[AioHttpIntegration(),
|
||||
SqlalchemyIntegration(),
|
||||
LoggingIntegration(event_level=None)],
|
||||
release=release)
|
||||
else:
|
||||
log.info("Sentry: disabled")
|
||||
|
||||
def get_secret(self, username: str):
|
||||
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
|
||||
|
||||
def set_secret(self, username: str, password: str):
|
||||
return keyring.set_password(f"Royalnet/{self.secrets_name}", username, password)
|
||||
|
||||
def run_blocking(self, address: str, port: int, verbose: bool):
|
||||
if verbose:
|
||||
core_logger = logging.root
|
||||
core_logger.setLevel(logging.DEBUG)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
|
||||
core_logger.addHandler(stream_handler)
|
||||
core_logger.debug("Logging setup complete.")
|
||||
self._init_sentry()
|
||||
# Run the server
|
||||
log.info(f"Running constellation server on {address}:{port}...")
|
||||
uvicorn.run(self.starlette, host=address, port=port)
|
8
royalnet/constellation/shoot.py
Normal file
8
royalnet/constellation/shoot.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
def shoot(code: int, description: str) -> JSONResponse:
|
||||
"""Create a error :class:`JSONResponse` with the passed error code and description."""
|
||||
return JSONResponse({
|
||||
"error": description
|
||||
}, status_code=code)
|
78
royalnet/constellation/star.py
Normal file
78
royalnet/constellation/star.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from typing import Type, TYPE_CHECKING, List, Union
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
if TYPE_CHECKING:
|
||||
from .constellation import Constellation
|
||||
|
||||
|
||||
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!"""
|
||||
tables: set = {}
|
||||
"""The set of :class`Alchemy` :class:`Table` classes required by this Star to function."""
|
||||
|
||||
def __init__(self, constellation: "Constellation"):
|
||||
self.constellation: "Constellation" = constellation
|
||||
|
||||
async def page(self, request: Request) -> Response:
|
||||
"""The function generating the :class:`Response` to a web :class:`Request`.
|
||||
|
||||
If it raises an error, the corresponding :class:`ExceptionStar` will be used to handle the request instead."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def alchemy(self):
|
||||
return self.constellation.alchemy
|
||||
|
||||
@property
|
||||
def Session(self):
|
||||
return self.constellation.alchemy.Session
|
||||
|
||||
@property
|
||||
def session_acm(self):
|
||||
return self.constellation.alchemy.session_acm
|
||||
|
||||
|
||||
class PageStar(Star):
|
||||
"""A PageStar is a class representing a single route of the website (for example, ``/api/user/get``).
|
||||
|
||||
To create a new website route you should create a new class inheriting from this class with a function overriding
|
||||
:meth:`page` and changing the values of :attr:`path` and optionally :attr:`methods` and :attr:`tables`."""
|
||||
path: str = NotImplemented
|
||||
"""The route of the star.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
path: str = '/api/user/get'
|
||||
|
||||
"""
|
||||
|
||||
methods: List[str] = ["GET"]
|
||||
"""The HTTP methods supported by the Star, in form of a list.
|
||||
|
||||
By default, a Star only supports the ``GET`` method, but more can be added.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
methods: List[str] = ["GET", "POST", "PUT", "DELETE"]
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ExceptionStar(Star):
|
||||
"""An ExceptionStar is a class that handles an :class:`Exception` raised by another star by returning a different
|
||||
response than the one originally intended.
|
||||
|
||||
The handled exception type is specified in the :attr:`error`.
|
||||
|
||||
It can also handle standard webserver errors, such as ``404 Not Found``:
|
||||
to handle them, set :attr:`error` to an :class:`int` of the corresponding error code.
|
||||
|
||||
To create a new exception handler you should create a new class inheriting from this class with a function
|
||||
overriding :meth:`page` and changing the values of :attr:`error` and optionally :attr:`tables`."""
|
||||
error: Union[Type[Exception], int]
|
||||
"""The error that should be handled by this star. It should be either a subclass of :exc:`Exception`,
|
||||
or the :class:`int` of an HTTP error code."""
|
|
@ -1,7 +0,0 @@
|
|||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
def error(code: int, description: str) -> JSONResponse:
|
||||
return JSONResponse({
|
||||
"error": description
|
||||
}, status_code=code)
|
|
@ -1,37 +0,0 @@
|
|||
import typing
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
if typing.TYPE_CHECKING:
|
||||
from .constellation import Constellation
|
||||
|
||||
|
||||
class Star:
|
||||
tables: set = {}
|
||||
|
||||
def __init__(self, constellation: "Constellation"):
|
||||
self.constellation: "Constellation" = constellation
|
||||
|
||||
async def page(self, request: Request) -> Response:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def alchemy(self):
|
||||
return self.constellation.alchemy
|
||||
|
||||
@property
|
||||
def Session(self):
|
||||
return self.constellation.alchemy.Session
|
||||
|
||||
@property
|
||||
def session_acm(self):
|
||||
return self.constellation.alchemy.session_acm
|
||||
|
||||
|
||||
class PageStar(Star):
|
||||
path: str = NotImplemented
|
||||
|
||||
methods: typing.List[str] = ["GET"]
|
||||
|
||||
|
||||
class ExceptionStar(Star):
|
||||
error: typing.Union[typing.Type[Exception], int]
|
Loading…
Reference in a new issue