From dac875988e364178a02fc1e6e99dbe3701372b51 Mon Sep 17 00:00:00 2001 From: dormant-user Date: Sat, 10 Aug 2024 17:30:48 -0500 Subject: [PATCH] Add CLI functionality and base pyproject.toml --- monitor/__init__.py | 67 ++++++++++++++++++++++++++++++++ monitor/auth.py | 7 ++-- monitor/main.py | 18 ++++++--- monitor/{routes.py => router.py} | 10 ++--- monitor/service.py | 17 ++++---- monitor/squire.py | 5 +-- pyproject.toml | 42 ++++++++++++++++++++ 7 files changed, 138 insertions(+), 28 deletions(-) rename monitor/{routes.py => router.py} (77%) create mode 100644 pyproject.toml diff --git a/monitor/__init__.py b/monitor/__init__.py index ca01444..a7d5609 100644 --- a/monitor/__init__.py +++ b/monitor/__init__.py @@ -1 +1,68 @@ +"""Placeholder for packaging.""" + +import sys + +import click + from monitor.main import start # noqa: F401 + +version = "0.0.0-a" + + +@click.command() +@click.argument("start", required=False) +@click.argument("run", required=False) +@click.option("--version", "-V", is_flag=True, help="Prints the version.") +@click.option("--help", "-H", is_flag=True, help="Prints the help section.") +@click.option( + "--env", + "-E", + type=click.Path(exists=True), + help="Environment configuration filepath.", +) +def commandline(*args, **kwargs) -> None: + """Starter function to invoke service-monitor via CLI commands. + + **Flags** + - ``--version | -V``: Prints the version. + - ``--help | -H``: Prints the help section. + - ``--env | -E``: Environment configuration filepath. + + **Commands** + ``start | run``: Initiates the backup process. + """ + assert sys.argv[0].endswith("monitor"), "Invalid commandline trigger!!" + options = { + "--version | -V": "Prints the version.", + "--help | -H": "Prints the help section.", + "--env | -E": "Environment configuration filepath.", + "start | run": "Initiates the backup process.", + } + # weird way to increase spacing to keep all values monotonic + _longest_key = len(max(options.keys())) + _pretext = "\n\t* " + choices = _pretext + _pretext.join( + f"{k} {'·' * (_longest_key - len(k) + 8)}→ {v}".expandtabs() + for k, v in options.items() + ) + if kwargs.get("version"): + click.echo(f"service-monitor {version}") + sys.exit(0) + if kwargs.get("help"): + click.echo( + f"\nUsage: monitor [arbitrary-command]\nOptions (and corresponding behavior):{choices}" + ) + sys.exit(0) + trigger = kwargs.get("start") or kwargs.get("run") + if trigger and trigger.lower() in ("start", "run"): + # Click doesn't support assigning defaults like traditional dictionaries, so kwargs.get("max", 100) won't work + start(env_file=kwargs.get("env")) + sys.exit(0) + elif trigger: + click.secho(f"\n{trigger!r} - Invalid command", fg="red") + else: + click.secho("\nNo command provided", fg="red") + click.echo( + f"Usage: monitor [arbitrary-command]\nOptions (and corresponding behavior):{choices}" + ) + sys.exit(1) diff --git a/monitor/auth.py b/monitor/auth.py index 480ea72..d7f4668 100644 --- a/monitor/auth.py +++ b/monitor/auth.py @@ -4,8 +4,7 @@ from fastapi import Depends from fastapi.security import HTTPBasicCredentials, HTTPBearer -from monitor.exceptions import APIResponse -from monitor.squire import settings +from monitor import exceptions, squire SECURITY = HTTPBearer() @@ -23,8 +22,8 @@ async def authenticator(token: HTTPBasicCredentials = Depends(SECURITY)) -> None auth = token.model_dump().get("credentials", "") if auth.startswith("\\"): auth = bytes(auth, "utf-8").decode(encoding="unicode_escape") - if secrets.compare_digest(auth, settings.apikey): + if secrets.compare_digest(auth, squire.settings.apikey): return - raise APIResponse( + raise exceptions.APIResponse( status_code=HTTPStatus.UNAUTHORIZED.real, detail=HTTPStatus.UNAUTHORIZED.phrase ) diff --git a/monitor/main.py b/monitor/main.py index b4041ca..8394005 100644 --- a/monitor/main.py +++ b/monitor/main.py @@ -1,16 +1,24 @@ +import os import platform import uvicorn from fastapi import FastAPI -from monitor.routes import routes -from monitor.squire import settings +from monitor import router, squire -def start() -> None: +def start(env_file: str = None) -> None: """Starter function for the API, which uses uvicorn server as trigger.""" + squire.settings = squire.Settings().from_env_file( + env_file=env_file + or os.environ.get("env_file") + or os.environ.get("ENV_FILE") + or ".env" + ) app = FastAPI( - routes=routes, + routes=router.routes, title=f"Service monitor for {platform.uname().node}", ) - uvicorn.run(host=settings.monitor_host, port=settings.monitor_port, app=app) + uvicorn.run( + host=squire.settings.monitor_host, port=squire.settings.monitor_port, app=app + ) diff --git a/monitor/routes.py b/monitor/router.py similarity index 77% rename from monitor/routes.py rename to monitor/router.py index 3ed7a98..fa29d5a 100644 --- a/monitor/routes.py +++ b/monitor/router.py @@ -4,9 +4,7 @@ from fastapi.responses import RedirectResponse from fastapi.routing import APIRoute -from monitor.auth import authenticator -from monitor.exceptions import APIResponse -from monitor.service import get_service_status +from monitor import auth, exceptions, service logging.getLogger("uvicorn.access").disabled = True LOGGER = logging.getLogger("uvicorn.error") @@ -14,7 +12,7 @@ async def service_monitor(service_name: str): """API function to monitor a service.""" - service_status = get_service_status(service_name) + service_status = service.get_service_status(service_name) LOGGER.info( "%s[%d]: %d - %s", service_name, @@ -22,7 +20,7 @@ async def service_monitor(service_name: str): service_status.status_code, service_status.description, ) - raise APIResponse( + raise exceptions.APIResponse( status_code=service_status.status_code, detail=service_status.description ) @@ -37,7 +35,7 @@ async def docs(): path="/service-monitor", endpoint=service_monitor, methods=["GET"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(auth.authenticator)], ), APIRoute(path="/", endpoint=docs, methods=["GET"], include_in_schema=False), ] diff --git a/monitor/service.py b/monitor/service.py index 92772da..a166a0d 100644 --- a/monitor/service.py +++ b/monitor/service.py @@ -4,13 +4,12 @@ import psutil -from monitor.exceptions import UnSupportedOS -from monitor.squire import ServiceStatus +from monitor import exceptions, squire current_os = platform.system() if current_os not in ("Darwin", "Linux", "Windows"): - raise UnSupportedOS( + raise exceptions.UnSupportedOS( f"{current_os} is unsupported.\n\t" "Host machine should either be macOS, Windows or any of Linux distros" ) @@ -31,7 +30,7 @@ def get_pid(service_name: str) -> int: return proc.info["pid"] -def get_service_status(service_name: str) -> ServiceStatus: +def get_service_status(service_name: str) -> squire.ServiceStatus: """Get service status. Args: @@ -45,25 +44,25 @@ def get_service_status(service_name: str) -> ServiceStatus: if not (pid := get_pid(service_name)): pid = 0000 - running = ServiceStatus( + running = squire.ServiceStatus( pid=pid, status_code=HTTPStatus.OK.real, description=f"{service_name} is running", ) - stopped = ServiceStatus( + stopped = squire.ServiceStatus( pid=pid, status_code=HTTPStatus.NOT_IMPLEMENTED.real, description=f"{service_name} has been stopped", ) - unknown = ServiceStatus( + unknown = squire.ServiceStatus( pid=pid, status_code=HTTPStatus.SERVICE_UNAVAILABLE.real, description=f"{service_name} - status unknwon", ) - unavailable = ServiceStatus( + unavailable = squire.ServiceStatus( pid=pid, status_code=HTTPStatus.NOT_FOUND.real, description=f"{service_name} - not found", @@ -93,7 +92,7 @@ def get_service_status(service_name: str) -> ServiceStatus: elif output == "inactive": return stopped else: - return ServiceStatus( + return squire.ServiceStatus( status_code=HTTPStatus.NOT_IMPLEMENTED.real, description=f"{service_name} - {output}", ) diff --git a/monitor/squire.py b/monitor/squire.py index 2286a72..19b5c48 100644 --- a/monitor/squire.py +++ b/monitor/squire.py @@ -1,4 +1,3 @@ -import os import socket from typing import Optional @@ -49,6 +48,4 @@ class Config: extra = "ignore" -settings = Settings().from_env_file( - env_file=os.environ.get("env_file") or os.environ.get("ENV_FILE") or ".env" -) +settings = Settings diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9c5a480 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[project] +name = "service-monitor" +dynamic = ["version", "dependencies"] +description = "OS agnostic service monitoring API" +readme = "README.md" +authors = [{ name = "Vignesh Rao", email = "svignesh1793@gmail.com" }] +license = { file = "LICENSE" } +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Development Status :: 5 - Production/Stable", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", +] +keywords = ["service-monitor"] +requires-python = ">=3.10" + +[tool.setuptools] +packages = ["monitor"] + +[tool.setuptools.dynamic] +version = {attr = "monitor.version"} +dependencies = { file = ["requirements.txt"] } + +[project.optional-dependencies] +dev = ["sphinx==5.1.1", "pre-commit", "recommonmark", "gitverse"] + +[project.scripts] +# sends all the args to commandline function, where the arbitary commands as processed accordingly +monitor = "monitor:commandline" + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project.urls] +Homepage = "https://github.com/thevickypedia/service-monitor" +Docs = "https://thevickypedia.github.io/service-monitor" +Source = "https://github.com/thevickypedia/service-monitor" +"Bug Tracker" = "https://github.com/thevickypedia/service-monitor/issues" +"Release Notes" = "https://github.com/thevickypedia/service-monitor/blob/main/release_notes.rst"