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">
|
<component name="JavaScriptSettings">
|
||||||
<option name="languageLevel" value="ES6" />
|
<option name="languageLevel" value="ES6" />
|
||||||
</component>
|
</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" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
2
TODO.md
2
TODO.md
|
@ -6,6 +6,6 @@
|
||||||
- [ ] interfaces
|
- [ ] interfaces
|
||||||
- [ ] packs (almost)
|
- [ ] packs (almost)
|
||||||
- [ ] utils
|
- [ ] utils
|
||||||
- [ ] web
|
- [x] constellation
|
||||||
- [ ] main
|
- [ ] main
|
||||||
- [ ] dependencies
|
- [ ] 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 = {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
|
psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
|
||||||
|
|
||||||
|
starlette = {version="0.12.13", optional=true}
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
telegram = ["python_telegram_bot"]
|
telegram = ["python_telegram_bot"]
|
||||||
|
@ -40,6 +42,7 @@ discord = ["discord_py", "pynacl"]
|
||||||
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
|
||||||
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
alchemy_hard = ["sqlalchemy", "psycopg2"]
|
||||||
bard = ["ffmpeg_python", "youtube_dl"]
|
bard = ["ffmpeg_python", "youtube_dl"]
|
||||||
|
constellation = ["starlette"]
|
||||||
|
|
||||||
# Development dependencies
|
# Development dependencies
|
||||||
[tool.poetry.dev-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 .constellation import Constellation
|
||||||
from .star import Star, PageStar, ExceptionStar
|
from .star import Star, PageStar, ExceptionStar
|
||||||
from .error import error
|
from .shoot import shoot
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Constellation",
|
"Constellation",
|
||||||
"Star",
|
"Star",
|
||||||
"PageStar",
|
"PageStar",
|
||||||
"ExceptionStar",
|
"ExceptionStar",
|
||||||
"error",
|
"shoot",
|
||||||
]
|
]
|
|
@ -15,6 +15,11 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Constellation:
|
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,
|
def __init__(self,
|
||||||
secrets_name: str,
|
secrets_name: str,
|
||||||
database_uri: str,
|
database_uri: str,
|
||||||
|
@ -31,35 +36,48 @@ class Constellation:
|
||||||
|
|
||||||
self.secrets_name: str = secrets_name
|
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)
|
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)
|
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:
|
for SelectedPageStar in page_stars:
|
||||||
|
log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}")
|
||||||
try:
|
try:
|
||||||
page_star_instance = SelectedPageStar(constellation=self)
|
page_star_instance = SelectedPageStar(constellation=self)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}")
|
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}!")
|
||||||
sentry_sdk.capture_exception(e)
|
raise
|
||||||
continue
|
|
||||||
log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}")
|
|
||||||
self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods)
|
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:
|
for SelectedExcStar in exc_stars:
|
||||||
|
log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}")
|
||||||
try:
|
try:
|
||||||
exc_star_instance = SelectedExcStar(constellation=self)
|
exc_star_instance = SelectedExcStar(constellation=self)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}")
|
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}!")
|
||||||
sentry_sdk.capture_exception(e)
|
raise
|
||||||
continue
|
|
||||||
log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}")
|
|
||||||
self.starlette.add_exception_handler(exc_star_instance.error, exc_star_instance.page)
|
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")
|
sentry_dsn = self.get_secret("sentry")
|
||||||
if sentry_dsn:
|
if sentry_dsn:
|
||||||
# noinspection PyUnreachableCode
|
# noinspection PyUnreachableCode
|
||||||
|
@ -68,28 +86,13 @@ class Constellation:
|
||||||
else:
|
else:
|
||||||
release = royalnet.version.semantic
|
release = royalnet.version.semantic
|
||||||
log.info(f"Sentry: enabled (Royalnet {release})")
|
log.info(f"Sentry: enabled (Royalnet {release})")
|
||||||
self.sentry = sentry_sdk.init(sentry_dsn,
|
sentry_sdk.init(sentry_dsn,
|
||||||
integrations=[AioHttpIntegration(),
|
integrations=[AioHttpIntegration(),
|
||||||
SqlalchemyIntegration(),
|
SqlalchemyIntegration(),
|
||||||
LoggingIntegration(event_level=None)],
|
LoggingIntegration(event_level=None)],
|
||||||
release=release)
|
release=release)
|
||||||
else:
|
else:
|
||||||
log.info("Sentry: disabled")
|
log.info("Sentry: disabled")
|
||||||
|
# Run the server
|
||||||
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()
|
|
||||||
log.info(f"Running constellation server on {address}:{port}...")
|
log.info(f"Running constellation server on {address}:{port}...")
|
||||||
uvicorn.run(self.starlette, host=address, port=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