Skip to content

Commit

Permalink
Add CLI functionality and base pyproject.toml
Browse files Browse the repository at this point in the history
  • Loading branch information
dormant-user committed Aug 10, 2024
1 parent b1b4a92 commit 7ef5ffc
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 28 deletions.
67 changes: 67 additions & 0 deletions monitor/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 3 additions & 4 deletions monitor/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
)
18 changes: 13 additions & 5 deletions monitor/main.py
Original file line number Diff line number Diff line change
@@ -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
)
10 changes: 4 additions & 6 deletions monitor/routes.py → monitor/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,23 @@
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")


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,
service_status.pid,
service_status.status_code,
service_status.description,
)
raise APIResponse(
raise exceptions.APIResponse(
status_code=service_status.status_code, detail=service_status.description
)

Expand All @@ -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),
]
17 changes: 8 additions & 9 deletions monitor/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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:
Expand All @@ -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",
Expand Down Expand Up @@ -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}",
)
Expand Down
5 changes: 1 addition & 4 deletions monitor/squire.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import socket
from typing import Optional

Expand Down Expand Up @@ -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
42 changes: 42 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]" }]
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"

0 comments on commit 7ef5ffc

Please sign in to comment.