Skip to content

Commit

Permalink
post 0.0.8 release (#30)
Browse files Browse the repository at this point in the history
* merge all changes

* changelog
  • Loading branch information
cav71 authored May 27, 2024
1 parent 9a947c5 commit 22ab2a3
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 27 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ Please, use the format:
-->

## [Unreleased]


## [0.0.9]

- asyncops: standardize module level variables TIMEOUT/RETRIES/RETRIES_DELAY
- asyncops: re-raise timeouts with the correct exception MinerCommandTimeoutError
- cli: handle flags for rexec (TIMEOUT/RETRIES/RETRIES_DELAY)
- cli: new log format
- cli: better support for exceptions captured in cli.cli
- utils: launch uses a base LuxosLaunchError for all exceptions


## [0.0.8]

- added a new cli.flags module to handle range flags (eg. allowing 127.0.0.1-127.0.0.9 ranges)
Expand Down
20 changes: 20 additions & 0 deletions docs/api/examples/simple2.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@

import argparse
import asyncio
import logging

import luxos.cli.v1 as cli

log = logging.getLogger(__name__)


def add_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument("-x", type=int, default=0, help="set the x flag")
Expand Down Expand Up @@ -45,6 +48,23 @@ def process_args(args: argparse.Namespace) -> argparse.Namespace | None:
async def main(args: argparse.Namespace):
"""a simple test script with a simple description"""

# many ways to abort a script
# 1. raising various exceptions
# (dump a stack trace)
# >>> raise RuntimeError("aborting")
# (dump a nice error message on the cli)
# >>> raise cli.AbortWrongArgument("a message)
# (abort unconditionally the application)
# >>> raise cli.AbortCliError("abort")
# 2. using args.error (nice cli error message)
# >>> args.error("too bad")

# logging to report messages
log.debug("a debug message")
log.info("an info message")
log.warning("a warning message")

# handle the args
if args.range:
print("args.range")
for host, port in args.range or []:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "luxos"
version = "0.0.8"
version = "0.0.9"
description = "The all encompassing LuxOS python library."
readme = "README.md"
license = { text = "MIT" } # TODO I don't think this is a MIT??
Expand Down
2 changes: 2 additions & 0 deletions src/luxos/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def logon_required(cmd: str, commands_list=COMMANDS) -> bool | None:


# TODO timeouts should be float | None
# TODO prepare for refactoring using example in
# tests.test_asyncops.test_bridge_execute_command
def execute_command(
host: str, port: int, timeout: int, cmd: str, params: list[str], verbose: bool
):
Expand Down
16 changes: 8 additions & 8 deletions src/luxos/asyncops.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
log = logging.getLogger(__name__)

TIMEOUT = 3.0 # default timeout for operations
RETRY = 0 # default number (>1) of retry on a failed operation
RETRY_DELAY = 1.0 # delay between retry
RETRIES = 0 # default number (>1) of retry on a failed operation
RETRIES_DELAY = 1.0 # delay between retry


def wrapped(function):
Expand Down Expand Up @@ -96,8 +96,8 @@ async def roundtrip(
-> (str) "{'STATUS': [{'Code': 22, 'Description': 'LUXminer 20 ..
"""
timeout = TIMEOUT if timeout is None else timeout
retry = RETRY if retry is None else retry
retry_delay = RETRY_DELAY if retry_delay is None else retry_delay
retry = RETRIES if retry is None else retry
retry_delay = RETRIES_DELAY if retry_delay is None else retry_delay

if not isinstance(cmd, (bytes, str)):
cmd = json.dumps(cmd, indent=2, sort_keys=True)
Expand All @@ -114,11 +114,11 @@ async def roundtrip(
return res
except (Exception, asyncio.TimeoutError) as e:
last_exception = e
if retry_delay:
if retry and retry_delay:
await asyncio.sleep(retry_delay)

if last_exception is not None:
raise last_exception
raise exceptions.MinerCommandTimeoutError(host, port) from last_exception


def validate_message(
Expand Down Expand Up @@ -259,8 +259,8 @@ async def rexec(
parameters = _rexec_parameters(parameters)

timeout = TIMEOUT if timeout is None else timeout
retry = RETRY if retry is None else retry
retry_delay = RETRY_DELAY if retry_delay is None else retry_delay
retry = RETRIES if retry is None else retry
retry_delay = RETRIES_DELAY if retry_delay is None else retry_delay

# if cmd is logon/logoff we dealt with it differently
if cmd in {"logon", "logoff"}:
Expand Down
26 changes: 22 additions & 4 deletions src/luxos/cli/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,36 @@ def type_hhmm(txt: str):


def add_arguments_rexec(parser: argparse.ArgumentParser) -> None:
"""adds the rexec timing for timeout/retries/delays
Ex.
def add_arguments(parser):
cli.flags.add_arguments_rexec(parser)
def process_args(args):
asyncops.TIMEOUT = args.timeout
asyncops.RETRIES = args.retries
asyncops.RETRIES_DELAY = args.retries_delay
return args
"""
from ..asyncops import RETRIES, RETRIES_DELAY, TIMEOUT

group = parser.add_argument_group(
"Remote execution", "rexec remote execution limits/timeouts"
)
group.add_argument(
"--timeout", type=float, default=3.0, help="Timeout for each command"
"--timeout", type=float, default=TIMEOUT, help="Timeout for each command"
)
group.add_argument(
"--max-retries",
"--retries",
type=int,
default=3,
default=RETRIES,
help="Maximum number of retries for each command",
)
group.add_argument(
"--delay-retry", type=float, default=3.0, help="Delay in s between retries"
"--retries-delay",
type=float,
default=RETRIES_DELAY,
help="Delay in s between retries",
)
48 changes: 37 additions & 11 deletions src/luxos/cli/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def main(parser: argparse.ArgumentParser):
import functools
import inspect
import logging
import logging.handlers
import sys
import time
from pathlib import Path
Expand All @@ -109,10 +110,23 @@ def main(parser: argparse.ArgumentParser):
"CONFIGPATH": Path("config.yaml"), # config default path
}


log = logging.getLogger(__name__)


class MyHandler(logging.StreamHandler):
def emit(self, record):
record.shortname = record.levelname[0]
return super().emit(record)


LOGGING_CONFIG = {
"format": "%(asctime)s [%(shortname)s] %(name)s: %(message)s",
"handlers": [
MyHandler(),
],
}


class CliBaseError(Exception):
pass

Expand Down Expand Up @@ -143,7 +157,20 @@ def setup_logging(config: dict[str, Any], count: int) -> None:
# we control if we go verbose or quite here
index = levelmap.index(level) + count
config["level"] = levelmap[max(min(index, n - 1), 0)]
logging.basicConfig(**config)

config2 = LOGGING_CONFIG.copy()
config2.update(config)
logging.basicConfig(**config2) # type: ignore


def log_sys_info():
from luxos import __hash__, __version__

log.info(
"py[%s], luxos[%s/%s]", sys.version.partition(" ")[0], __version__, __hash__
)
log.debug("interpreter: %s", sys.executable)
log.debug("version: %s", sys.version)


class LuxosParser(argparse.ArgumentParser):
Expand Down Expand Up @@ -190,7 +217,7 @@ def parse_args(self, args=None, namespace=None):

count = (options.verbose or 0) - (options.quiet or 0)
setup_logging(config, count)

log_sys_info()
return options

@classmethod
Expand Down Expand Up @@ -251,13 +278,18 @@ def setup():
kwargs["args"] = args
yield sig.bind(**kwargs)
except AbortCliError as exc:
errormsg = str(exc)
success = "failed"
show_timing = False
if exc.args:
print(str(exc), file=sys.stderr)
sys.exit(2)
except AbortWrongArgument as exc:
show_timing = False
parser.print_usage(sys.stderr)
print(f"{parser.prog}: error: {exc.args[0]}", file=sys.stderr)
sys.exit(2)
except SystemExit as exc:
show_timing = False
sys.exit(exc.code)
except Exception:
log.exception("un-handled exception")
success = "failed"
Expand All @@ -268,24 +300,18 @@ def setup():
if errormsg:
parser.error(errormsg)

def log_sys_info():
log.debug("interpreter: %s", sys.executable)
log.debug("version: %s", sys.version)

if inspect.iscoroutinefunction(function):

@functools.wraps(function)
async def _cli2(*args, **kwargs):
with setup() as ba:
log_sys_info()
return await function(*ba.args, **ba.kwargs)

else:

@functools.wraps(function)
def _cli2(*args, **kwargs):
with setup() as ba:
log_sys_info()
return function(*ba.args, **ba.kwargs)

_cli2.attributes = { # type: ignore[attr-defined]
Expand Down
5 changes: 4 additions & 1 deletion src/luxos/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import asyncio


class LuxosBaseException(Exception):
pass

Expand All @@ -18,7 +21,7 @@ class MinerCommandSessionAlreadyActive(MinerConnectionError):
pass


class MinerCommandTimeoutError(MinerConnectionError):
class MinerCommandTimeoutError(MinerConnectionError, asyncio.TimeoutError):
pass


Expand Down
32 changes: 30 additions & 2 deletions src/luxos/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
from __future__ import annotations

import asyncio
import functools
from typing import Any, Callable

import luxos.misc
from luxos.asyncops import rexec # noqa: F401

# we bring here functions from other modules
from luxos.asyncops import rexec # noqa: F401
from luxos.exceptions import MinerConnectionError
from luxos.ips import load_ips_from_csv # noqa: F401

# TODO prepare for refactoring using example in
# tests.test_asyncops.test_bridge_execute_command
from luxos.scripts.luxos import execute_command # noqa: F401


class LuxosLaunchError(MinerConnectionError):
pass


def ip_ranges(
txt: str, rsep: str = "-", gsep: str = ":"
) -> list[tuple[str, int | None]]:
Expand Down Expand Up @@ -41,17 +50,36 @@ async def launch(
) -> Any:
"""launch an async function on a list of (host, port) miners
Special kwargs:
- batch execute operation in group of batch tasks (rate limiting)
- naked do not wrap the call, so is up to you catching exceptions (default None)
Eg.
async printme(host, port, value):
print(await rexec(host, port, "version"))
asyncio.run(launch([("127.0.0.1", 4028)], printme, value=11, batch=10))
"""
# special kwargs!!

# a naked options, wraps the 'call' and re-raise exceptions as LuxosLaunchError
naked = kwargs.pop("naked") if "naked" in kwargs else None

def wraps(fn):
@functools.wraps(fn)
async def _fn(host: str, port: int):
try:
return await fn(host, port)
except Exception as exc:
raise LuxosLaunchError(host, port) from exc

return _fn

n = int(kwargs.pop("batch") or 0) if "batch" in kwargs else None
if n and n < 0:
raise RuntimeError(
f"cannot pass the 'batch' keyword argument with a value < 0: batch={n}"
)
if not naked:
call = wraps(call)

if n:
result = []
Expand Down
32 changes: 32 additions & 0 deletions tests/test_asyncops.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import contextlib
import os

import pytest
Expand Down Expand Up @@ -191,3 +192,34 @@ async def test_miner_profile_sets():
expected = {p["Profile Name"] for p in profiles}
found = {p["Profile Name"] for p in profiles2}
assert found == expected


@pytest.mark.asyncio
async def test_roundtrip_timeout():
"""checks roundrtip sends and receive a message (1-listener)"""
host, port = "127.0.0.99", 12345
with pytest.raises(aapi.exceptions.MinerCommandTimeoutError):
await aapi.rexec(host, port, "hello", timeout=0.5)


@pytest.mark.skipif(not getminer(), reason="need to set LUXOS_TEST_MINER")
def test_bridge_execute_command():
from luxos.scripts.luxos import execute_command
from luxos.utils import rexec

# get the initial profile list
host, port = getminer()

def adapter(awaitable):
with contextlib.suppress(asyncio.TimeoutError):
loop = asyncio.get_event_loop()
return loop.run_until_complete(awaitable)

out = execute_command(host, port, 3, "profiles", parameters=[], verbose=True)
out1 = adapter(rexec(host, port, cmd="profiles"))
assert out["PROFILES"] == out1["PROFILES"]

port += 1
out = execute_command(host, port, 3, "profiles", parameters=[], verbose=True)
out1 = adapter(rexec(host, port, cmd="profiles"))
assert out == out1

0 comments on commit 22ab2a3

Please sign in to comment.