diff --git a/royalnet/backpack/__init__.py b/royalnet/backpack/__init__.py index a51af2ed..3d320ba5 100644 --- a/royalnet/backpack/__init__.py +++ b/royalnet/backpack/__init__.py @@ -1,19 +1 @@ """A Pack that is imported by default by all Royalnet instances.""" - -from . import commands, tables, stars, events -from .commands import available_commands -from .tables import available_tables -from .stars import available_page_stars, available_exception_stars -from .events import available_events - -__all__ = [ - "commands", - "tables", - "stars", - "events", - "available_commands", - "available_tables", - "available_page_stars", - "available_exception_stars", - "available_events", -] diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index 49192e58..129ed92b 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -7,10 +7,5 @@ available_page_stars = [ ApiRoyalnetVersionStar, ] -# Enter the ExceptionStars of your Pack here! -available_exception_stars = [ - -] - # Don't change this, it should automatically generate __all__ -__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]] +__all__ = [star.__name__ for star in available_page_stars] diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index 26f3b9d0..a842ddfe 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -1,15 +1,11 @@ -import royalnet -from starlette.requests import Request -from starlette.responses import * -from royalnet.constellation import PageStar +import royalnet.version as rv +from royalnet.constellation import ApiStar -class ApiRoyalnetVersionStar(PageStar): +class ApiRoyalnetVersionStar(ApiStar): path = "/api/royalnet/version" - async def page(self, request: Request) -> JSONResponse: - return JSONResponse({ - "version": { - "semantic": royalnet.__version__, - } - }) + async def api(self, data: dict) -> dict: + return { + "semantic": rv.semantic + } diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py new file mode 100644 index 00000000..3d87e1ef --- /dev/null +++ b/royalnet/backpack/tables/tokens.py @@ -0,0 +1,28 @@ +import datetime +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declared_attr + + +class Token: + __tablename__ = "tokens" + + @declared_attr + def token(self): + return Column(String, primary_key=True) + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("users.uid"), nullable=False) + + @declared_attr + def user(self): + return relationship("User", backref="tokens") + + @declared_attr + def expiration(self): + return Column(DateTime, nullable=False) + + @property + def expired(self): + return datetime.datetime.now() > self.expiration diff --git a/royalnet/constellation/__init__.py b/royalnet/constellation/__init__.py index 69c62af0..d9438019 100644 --- a/royalnet/constellation/__init__.py +++ b/royalnet/constellation/__init__.py @@ -15,13 +15,17 @@ You can install them with: :: """ from .constellation import Constellation -from .star import Star, PageStar, ExceptionStar -from .shoot import shoot +from .star import Star +from .pagestar import PageStar +from .apistar import ApiStar +from .jsonapi import api_response, api_success, api_error __all__ = [ "Constellation", "Star", "PageStar", - "ExceptionStar", - "shoot", + "ApiStar", + "api_response", + "api_success", + "api_error", ] diff --git a/royalnet/constellation/apistar.py b/royalnet/constellation/apistar.py new file mode 100644 index 00000000..372af97e --- /dev/null +++ b/royalnet/constellation/apistar.py @@ -0,0 +1,26 @@ +from typing import * +from json import JSONDecodeError +from abc import * +from starlette.requests import Request +from starlette.responses import JSONResponse +from .pagestar import PageStar +from .jsonapi import api_error, api_success + + +class ApiStar(PageStar, ABC): + async def page(self, request: Request) -> JSONResponse: + if request.query_params: + data = request.query_params + else: + try: + data = await request.json() + except JSONDecodeError: + data = {} + try: + response = await self.api(data) + except Exception as e: + return api_error(e) + return api_success(response) + + async def api(self, data: dict) -> dict: + raise NotImplementedError() diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py index 6fad39b9..2cdc94e9 100644 --- a/royalnet/constellation/constellation.py +++ b/royalnet/constellation/constellation.py @@ -8,7 +8,7 @@ import royalnet.alchemy as ra import royalnet.herald as rh import royalnet.utils as ru import royalnet.commands as rc -from .star import PageStar, ExceptionStar +from .pagestar import PageStar from ..utils import init_logging @@ -42,7 +42,12 @@ class Constellation: for pack_name in pack_names: log.debug(f"Importing pack: {pack_name}") try: - packs[pack_name] = importlib.import_module(pack_name) + packs[pack_name] = { + "commands": importlib.import_module(f"{pack_name}.commands"), + "events": importlib.import_module(f"{pack_name}.events"), + "stars": importlib.import_module(f"{pack_name}.stars"), + "tables": importlib.import_module(f"{pack_name}.tables"), + } except ImportError as e: log.error(f"Error during the import of {pack_name}: {e}") log.info(f"Packs: {len(packs)} imported") @@ -60,7 +65,7 @@ class Constellation: tables = set() for pack in packs.values(): try: - tables = tables.union(pack.available_tables) + tables = tables.union(pack["tables"].available_tables) except AttributeError: log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.") continue @@ -99,7 +104,7 @@ class Constellation: pack = packs[pack_name] pack_cfg = packs_cfg.get(pack_name, {}) try: - events = pack.available_events + events = pack["events"].available_events except AttributeError: log.warning(f"Pack `{pack}` does not have the `available_events` attribute.") else: @@ -118,17 +123,11 @@ class Constellation: pack = packs[pack_name] pack_cfg = packs_cfg.get(pack_name, {}) try: - page_stars = pack.available_page_stars + page_stars = pack["stars"].available_page_stars except AttributeError: log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.") else: self.register_page_stars(page_stars, pack_cfg) - try: - exc_stars = pack.available_exception_stars - except AttributeError: - log.warning(f"Pack `{pack}` does not have the `available_exception_stars` attribute.") - else: - self.register_exc_stars(exc_stars, pack_cfg) log.info(f"PageStars: {len(self.starlette.routes)} stars") log.info(f"ExceptionStars: {len(self.starlette.exception_handlers)} stars") @@ -259,14 +258,6 @@ class Constellation: 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__}") @@ -279,18 +270,6 @@ class Constellation: continue 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 SelectedExcStar in exc_stars: - log.debug(f"Registering: {SelectedExcStar.error} -> {SelectedExcStar.__qualname__}") - try: - exc_star_instance = SelectedExcStar(interface=self.Interface(pack_cfg)) - except Exception as e: - log.error(f"Skipping: " - f"{SelectedExcStar.__qualname__} - {e.__class__.__qualname__} in the initialization.") - ru.sentry_exc(e) - continue - 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}/...") loop: aio.AbstractEventLoop = aio.get_event_loop() diff --git a/royalnet/constellation/jsonapi.py b/royalnet/constellation/jsonapi.py new file mode 100644 index 00000000..e46b3d28 --- /dev/null +++ b/royalnet/constellation/jsonapi.py @@ -0,0 +1,32 @@ +from typing import * +try: + from starlette.responses import JSONResponse +except ImportError: + JSONResponse = None + + +def api_response(data: dict, code: int, headers: dict = None) -> JSONResponse: + if headers is None: + headers = {} + full_headers = { + **headers, + "Access-Control-Allow-Origin": "*", + } + return JSONResponse(data, status_code=code, headers=full_headers) + + +def api_success(data: dict) -> JSONResponse: + result = { + "success": True, + "data": data + } + return api_response(result, code=200) + + +def api_error(error: Exception, code: int = 500) -> JSONResponse: + result = { + "success": False, + "error_type": error.__class__.__qualname__, + "error_args": list(error.args) + } + return api_response(result, code=code) diff --git a/royalnet/constellation/pagestar.py b/royalnet/constellation/pagestar.py new file mode 100644 index 00000000..79da65ab --- /dev/null +++ b/royalnet/constellation/pagestar.py @@ -0,0 +1,34 @@ +from typing import * +from .star import Star + + +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`, :attr:`.path` and optionally :attr:`.methods`.""" + + 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"] + + """ + + def __repr__(self): + return f"<{self.__class__.__qualname__}: {self.path}>" diff --git a/royalnet/constellation/shoot.py b/royalnet/constellation/shoot.py deleted file mode 100644 index f5c8471e..00000000 --- a/royalnet/constellation/shoot.py +++ /dev/null @@ -1,13 +0,0 @@ -try: - from starlette.responses import JSONResponse -except ImportError: - JSONResponse = None - - -def shoot(code: int, description: str) -> JSONResponse: - """Create a error :class:`~starlette.response.JSONResponse` with the passed error code and description.""" - if JSONResponse is None: - raise ImportError("'constellation' extra is not installed") - return JSONResponse({ - "error": description - }, status_code=code) diff --git a/royalnet/constellation/star.py b/royalnet/constellation/star.py index 657968dc..c56fa27e 100644 --- a/royalnet/constellation/star.py +++ b/royalnet/constellation/star.py @@ -48,63 +48,3 @@ class Star: def __repr__(self): return f"<{self.__class__.__qualname__}>" - - -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`.""" - 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"] - - """ - - def __repr__(self): - return f"<{self.__class__.__qualname__}: {self.path}>" - - -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 value of :attr:`.error`.""" - 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. - - Examples: - :: - - error: int = 404 - - :: - - error: Type[Exception] = ValueError - """ - - def __repr__(self): - return f"<{self.__class__.__qualname__}: handles {self.error}>" diff --git a/royalnet/generate.py b/royalnet/generate.py index 5462d22d..80e18944 100644 --- a/royalnet/generate.py +++ b/royalnet/generate.py @@ -19,7 +19,12 @@ def run(config_filename, file_format): packs = {} for pack_name in pack_names: try: - packs[pack_name] = importlib.import_module(pack_name) + packs[pack_name] = { + "commands": importlib.import_module(f"{pack_name}.commands"), + "events": importlib.import_module(f"{pack_name}.events"), + "stars": importlib.import_module(f"{pack_name}.stars"), + "tables": importlib.import_module(f"{pack_name}.tables"), + } except ImportError as e: p(f"Skipping `{pack_name}`: {e}", err=True) continue @@ -30,7 +35,7 @@ def run(config_filename, file_format): lines = [] try: - commands = pack.available_commands + commands = pack["commands"].available_commands except AttributeError: p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True) continue @@ -41,93 +46,6 @@ def run(config_filename, file_format): for line in lines: p(line) - elif file_format == "markdown": - p("") - p("") - for pack_name in packs: - pack = packs[pack_name] - p(f"# `{pack_name}`") - p("") - if pack.__doc__: - p(f"{pack.__doc__}") - p("") - - try: - commands = pack.available_commands - except AttributeError: - p(f"Pack `{pack}` does not have the `available_commands` attribute.", err=True) - else: - p(f"## Commands") - p("") - for command in commands: - p(f"### `{command.name}`") - p("") - p(f"{command.description}") - p("") - if command.__doc__: - p(f"{command.__doc__}") - p("") - if len(command.aliases) > 0: - p(f"> Aliases: {''.join(['`' + alias + '` ' for alias in command.aliases])}") - p("") - - try: - events = pack.available_events - except AttributeError: - p(f"Pack `{pack}` does not have the `available_events` attribute.", err=True) - else: - p(f"## Events") - p("") - for event in events: - p(f"### `{event.name}`") - p("") - if event.__doc__: - p(f"{event.__doc__}") - p("") - - try: - page_stars = pack.available_page_stars - except AttributeError: - p(f"Pack `{pack}` does not have the `available_page_stars` attribute.", err=True) - else: - p(f"## Page Stars") - p("") - for page_star in page_stars: - p(f"### `{page_star.path}`") - p("") - if page_star.__doc__: - p(f"{page_star.__doc__}") - p("") - - try: - exc_stars = pack.available_exception_stars - except AttributeError: - p(f"Pack `{pack}` does not have the `available_exception_stars` attribute.", err=True) - else: - p(f"## Exception Stars") - p("") - for exc_star in exc_stars: - p(f"### `{exc_star.error}`") - p("") - if exc_star.__doc__: - p(f"{exc_star.__doc__}") - p("") - - try: - tables = pack.available_tables - except AttributeError: - p(f"Pack `{pack}` does not have the `available_tables` attribute.", err=True) - else: - p(f"## Tables") - p("") - for table in tables: - p(f"### `{table.__tablename__}`") - p("") - # TODO: list columns - if table.__doc__: - p(f"{table.__doc__}") - p("") - else: raise click.ClickException("Unknown format") diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index 39c5c3d6..022763ff 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -7,7 +7,7 @@ from sqlalchemy.schema import Table from royalnet.commands import * import royalnet.utils as ru import royalnet.alchemy as ra -import royalnet.backpack as rb +import royalnet.backpack.tables as rbt import royalnet.herald as rh import traceback @@ -20,7 +20,7 @@ class Serf: Discord).""" interface_name = NotImplemented - _master_table: type = rb.tables.User + _master_table: type = rbt.User _identity_table: type = NotImplemented _identity_column: str = NotImplemented @@ -39,7 +39,12 @@ class Serf: for pack_name in pack_names: log.debug(f"Importing pack: {pack_name}") try: - packs[pack_name] = importlib.import_module(pack_name) + packs[pack_name] = { + "commands": importlib.import_module(f"{pack_name}.commands"), + "events": importlib.import_module(f"{pack_name}.events"), + "stars": importlib.import_module(f"{pack_name}.stars"), + "tables": importlib.import_module(f"{pack_name}.tables"), + } except ImportError as e: log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n" f"{traceback.format_exception(*sys.exc_info())}") @@ -68,7 +73,7 @@ class Serf: tables = set() for pack in packs.values(): try: - tables = tables.union(pack.available_tables) + tables = tables.union(pack["tables"].available_tables) except AttributeError: log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.") continue @@ -95,13 +100,13 @@ class Serf: pack = packs[pack_name] pack_cfg = packs_cfg.get(pack_name, {}) try: - events = pack.available_events + events = pack["events"].available_events except AttributeError: log.warning(f"Pack `{pack}` does not have the `available_events` attribute.") else: self.register_events(events, pack_cfg) try: - commands = pack.available_commands + commands = pack["commands"].available_commands except AttributeError: log.warning(f"Pack `{pack}` does not have the `available_commands` attribute.") else: