From 6318e96ee8b9070a599e6223b70e9a87b8c875ae Mon Sep 17 00:00:00 2001 From: Alex Povel Date: Sun, 24 Jul 2022 17:34:09 +0200 Subject: [PATCH] feat(cli): Add command to serve a single file Also refactor classes a bit, such that everything is now instance- as opposed to class-level. Seemed more streamlined. Closes #14. --- ancv/__main__.py | 21 ++++++++++++-- ancv/web/server.py | 68 +++++++++++++++++++++++++++++++++------------- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/ancv/__main__.py b/ancv/__main__.py index f9ddc80..88191fc 100644 --- a/ancv/__main__.py +++ b/ancv/__main__.py @@ -4,7 +4,6 @@ locally. """ -import json import logging import os from pathlib import Path @@ -17,7 +16,7 @@ from ancv.utils.exceptions import ResumeConfigError from ancv.visualization.templates import Template -from ancv.web.server import API +from ancv.web.server import APIHandler, FileHandler, ServerContext app = typer.Typer(no_args_is_help=True, help=__doc__) server_app = typer.Typer(no_args_is_help=True, help="Interacts with the web server.") @@ -35,7 +34,23 @@ def api( ) -> None: """Starts the web server and serves the API.""" - API.run(host=host, port=port, path=path) + context = ServerContext(host=host, port=port, path=path) + APIHandler().run(context) + + +@server_app.command() +def file( + file: Path = typer.Argument(Path("resume.json")), + host: str = typer.Option("0.0.0.0", help="Hostname to bind to."), + port: int = typer.Option(8080, help="Port to bind to."), + path: Optional[str] = typer.Option( + None, help="File system path for an HTTP server UNIX domain socket." + ), +) -> None: + """Starts the web server and serves a single, rendered resume file.""" + + context = ServerContext(host=host, port=port, path=path) + FileHandler(file).run(context) @app.command() diff --git a/ancv/web/server.py b/ancv/web/server.py index d3e405f..7032e9f 100644 --- a/ancv/web/server.py +++ b/ancv/web/server.py @@ -1,5 +1,7 @@ import os from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path from typing import AsyncGenerator, Optional from aiohttp import ClientSession, web @@ -16,25 +18,36 @@ LOGGER = get_logger() -class Server(ABC): - @classmethod - @abstractmethod - def run(cls, host: Optional[str], port: Optional[int], path: Optional[str]) -> None: - pass +@dataclass +class ServerContext: + host: Optional[str] + port: Optional[int] + path: Optional[str] + +class Runnable(ABC): + @abstractmethod + def run(self, context: ServerContext) -> None: + ... -class API(Server): - ROUTES = web.RouteTableDef() - @classmethod - def run(cls, host: Optional[str], port: Optional[int], path: Optional[str]) -> None: +class APIHandler(Runnable): + def run(self, context: ServerContext) -> None: LOGGER.debug("Instantiating web application.") app = web.Application() + LOGGER.debug("Adding routes.") - app.add_routes(cls.ROUTES) - app.cleanup_ctx.append(cls.app_context) + app.add_routes( + [ + web.get("/", self.root), + web.get("/{username}", self.username), + ] + ) + + app.cleanup_ctx.append(self.app_context) + LOGGER.info("Loaded, starting server...") - web.run_app(app, host=host, port=port, path=path) + web.run_app(app, host=context.host, port=context.port, path=context.path) @staticmethod async def app_context(app: web.Application) -> AsyncGenerator[None, None]: @@ -81,9 +94,7 @@ async def app_context(app: web.Application) -> AsyncGenerator[None, None]: log.info("App context teardown done.") - @ROUTES.get("/") - @staticmethod - async def root(request: web.Request) -> web.Response: + async def root(self, request: web.Request) -> web.Response: user_agent = request.headers.get("User-Agent", "") HOMEPAGE = os.environ.get("HOMEPAGE", METADATA.home_page or "") @@ -101,10 +112,8 @@ async def root(request: web.Request) -> web.Response: raise web.HTTPFound(browser_page) # Redirect - @ROUTES.get("/{username}") - @staticmethod - async def username(request: web.Request) -> web.Response: - log = LOGGER + async def username(self, request: web.Request) -> web.Response: + log = LOGGER.bind(request=request) log.info(request.message.headers) user = request.match_info["username"] @@ -128,4 +137,25 @@ async def username(request: web.Request) -> web.Response: except ResumeConfigError as e: log.warning(str(e)) return web.Response(text=str(e)) + log.debug("Serving rendered template.") return web.Response(text=template.render()) + + +class FileHandler(Runnable): + def __init__(self, file: Path) -> None: + self.template = Template.from_file(file) + self.rendered = self.template.render() + + def run(self, context: ServerContext) -> None: + LOGGER.debug("Instantiating web application.") + app = web.Application() + + LOGGER.debug("Adding routes.") + app.add_routes([web.get("/", self.root)]) + + LOGGER.info("Loaded, starting server...") + web.run_app(app, host=context.host, port=context.port, path=context.path) + + async def root(self, request: web.Request) -> web.Response: + LOGGER.debug("Serving rendered template.", request=request) + return web.Response(text=self.rendered)