diff options
Diffstat (limited to 'gamechestcli')
22 files changed, 636 insertions, 188 deletions
diff --git a/gamechestcli/cli.py b/gamechestcli/gamechest/cli.py index 8b13789..8b13789 100644 --- a/gamechestcli/cli.py +++ b/gamechestcli/gamechest/cli.py diff --git a/gamechestcli/gamechest/cliactions/install.py b/gamechestcli/gamechest/cliactions/install.py new file mode 100644 index 0000000..ed62760 --- /dev/null +++ b/gamechestcli/gamechest/cliactions/install.py @@ -0,0 +1,31 @@ +import functools + +from .. import gamedb +from .. import paths +from .. import processor +from ..runners.install import Install +from ..statusdb import StatusDB + + +def install_game(status_db, game_info): + remote_basedir = paths.get_remote_basedir() + source = f'{remote_basedir}/{game_info["package_name"]}' + dest = paths.get_games_install_basedir() + title = 'Installing game...' + task = functools.partial(Install, source, dest) + processor.process_task(title, task) + status_db.set_installed(game_info) + + +def install(game_id): + game_info = gamedb.get_game_info(game_id) + status_db = StatusDB() + if status_db.is_installed(game_info): + # games is already installed + return + install_game(status_db, game_info) + + +if __name__ == "__main__": + import sys + install(sys.argv[1]) diff --git a/gamechestcli/gamechest/cliactions/remove.py b/gamechestcli/gamechest/cliactions/remove.py new file mode 100644 index 0000000..c187ae1 --- /dev/null +++ b/gamechestcli/gamechest/cliactions/remove.py @@ -0,0 +1,38 @@ +import contextlib +import functools +import selectors + +from rich.progress import Progress as RichProgress +from rich import print + +from .. import gamedb +from .. import paths +from .. import processor +from ..runners.remove import Remove +from ..statusdb import StatusDB + + +def remove_game(status_db, game_info): + remote_basedir = paths.get_remote_basedir() + path = ( + paths.get_games_install_basedir() + / status_db.get_installed_game_name(game_info['id']) + ) + title = 'Removing game...' + task = functools.partial(Remove, path) + processor.process_task(title, task) + status_db.set_uninstalled(game_info) + + +def remove(game_id): + game_info = gamedb.get_game_info(game_id) + status_db = StatusDB() + if status_db.is_installed(game_info): + remove_game(status_db, game_info) + else: + print('Game not installed') + + +if __name__ == "__main__": + import sys + remove(sys.argv[1]) diff --git a/gamechestcli/__init__.py b/gamechestcli/gamechest/cliactions/run.py index e69de29..e69de29 100644 --- a/gamechestcli/__init__.py +++ b/gamechestcli/gamechest/cliactions/run.py diff --git a/gamechestcli/gamechest/config.py b/gamechestcli/gamechest/config.py new file mode 100644 index 0000000..38b02fd --- /dev/null +++ b/gamechestcli/gamechest/config.py @@ -0,0 +1,50 @@ +from pathlib import Path +from dataclasses import dataclass, asdict + +import yaml +from xdg.BaseDirectory import load_first_config, save_config_path, xdg_data_home + +CONFIG_PATH_TUPLE = ('org.devys.gamechest', 'config.yaml') + + +@dataclass +class GamechestConfig: + + # TODO issue: having a Path() here raise yaml dump error when saving + games_path: Path = Path(xdg_data_home) + #games_path: str + + @classmethod + def load_config_from_path(cls, path): + #import dataconf + #conf = dataconf.file('test.yaml', GamechestConfig) + #print(conf.games_path) + with Path(path).open() as fdin: + config_yaml = yaml.safe_load(fdin) + for key in ('games_path', ): + config_yaml[key] = Path(config_yaml[key]).expanduser() + return cls(**config_yaml) + + @classmethod + def load_config(cls): + first_config_path = load_first_config(*CONFIG_PATH_TUPLE) + return_config = None + if first_config_path is None: + return_config = GamechestConfig() + return_config.save_config() + else: + return_config = cls.load_config_from_path(first_config_path) + return return_config + + def save_config(self): + save_path = ( + Path(save_config_path(*CONFIG_PATH_TUPLE[:-1])) + / CONFIG_PATH_TUPLE[-1] + ) + with save_path.open('w', encoding='utf8') as fdout: + yaml.safe_dump(asdict(self), fdout) + + +if __name__ == "__main__": + #print(GamechestConfig.load_config_from_path('test.yaml')) + print(GamechestConfig.load_config()) diff --git a/gamechestcli/gamechest/consts.py b/gamechestcli/gamechest/consts.py new file mode 100644 index 0000000..091fd70 --- /dev/null +++ b/gamechestcli/gamechest/consts.py @@ -0,0 +1,2 @@ +XDG_RESOURCE_NAME = 'org.devys.gamechest' +STATUS_DB_NAME = 'status.sqlite' diff --git a/gamechestcli/gamechest/gamedb.py b/gamechestcli/gamechest/gamedb.py new file mode 100644 index 0000000..1e3967c --- /dev/null +++ b/gamechestcli/gamechest/gamedb.py @@ -0,0 +1,16 @@ +import yaml + +from . import paths + + +def load_games_database(): + database_path = paths.get_games_database_path() + with open(database_path, 'rb') as fdin: + return yaml.safe_load(fdin) + + +def get_game_info(game_id): + db = load_games_database() + return next(game_info + for game_info in db['games'] + if game_info['id'] == game_id) diff --git a/gamechestcli/gamechest/paths.py b/gamechestcli/gamechest/paths.py new file mode 100644 index 0000000..53cafe0 --- /dev/null +++ b/gamechestcli/gamechest/paths.py @@ -0,0 +1,23 @@ +import os + +from xdg import xdg_data_home + +from . import consts + + +def get_games_database_path(): + # TODO: unhardcode this + #return os.path.expanduser('~/games/.saves/gamedata.yaml') + return os.path.expanduser('~/game-saves/gamedata.yaml') + + +def get_remote_basedir(): + # TODO: unhardcode this + return 'jibril:/storage/games' + + +def get_games_install_basedir(): + games_install_path = xdg_data_home() / consts.XDG_RESOURCE_NAME / 'games' + games_install_path.mkdir(parents=True, exist_ok=True) + return games_install_path + diff --git a/gamechestcli/gamechest/processor.py b/gamechestcli/gamechest/processor.py new file mode 100644 index 0000000..794deb8 --- /dev/null +++ b/gamechestcli/gamechest/processor.py @@ -0,0 +1,41 @@ +import contextlib +import selectors + +from rich.progress import Progress as RichProgress +from rich import print + + +def process_task(title, task): + with contextlib.ExitStack() as stack: + runner = stack.enter_context(task()) + selector = stack.enter_context(selectors.DefaultSelector()) + selector.register(runner.get_read_fd(), selectors.EVENT_READ) + rich_progress = stack.enter_context(RichProgress()) + known_total = 100 + global_id = rich_progress.add_task(title, total=known_total) + last_step = None + while (rc := runner.poll()) is None: + if selector.select(): + progress = runner.progress_read() + #rich_progress.console.log(progress) + known_total = progress.steps_count*100 + rich_progress.update(global_id, + completed=progress.step*100+progress.percent, + total=known_total) + if last_step != progress.step: + if last_step is not None: + rich_progress.update(step_id, completed=100) + else: + rich_progress.console.print('Total steps:', + progress.steps_count) + last_step = progress.step + step_id = rich_progress.add_task( + f'Step {progress.step}', total=100) + rich_progress.update(step_id, completed=progress.percent) + rich_progress.update(step_id, completed=100) + rich_progress.update(global_id, completed=known_total) + #rich_progress.console.print('installation ended with code:', rc) + if rc != 0: + # success, update db to say installed + status_db.set_installed(game_info) + print('ended with code:', rc) diff --git a/gamechestcli/gamechest/runners/download.py b/gamechestcli/gamechest/runners/download.py new file mode 100644 index 0000000..0dfbd05 --- /dev/null +++ b/gamechestcli/gamechest/runners/download.py @@ -0,0 +1,68 @@ +import os +import re +import subprocess + +import humanfriendly + +from ..structures import Progress +from .runnerbase import RunnerBase, neutral_locale_variables + + +class Download(RunnerBase): + + _rsync_progress_re = re.compile(r'^\s*(\S+)\s+(\d+)%\s+(\S+)\s+(\S+)\s+$') + + def __init__(self, src, dst): + self.proc = subprocess.Popen( + [ + 'rsync', + '--partial', + # --no-h: not human, easier to parse (but speed still appears + # in human form). + '--no-h', + '--info=progress2', + src, + dst, + ], + stdout=subprocess.PIPE, + encoding='utf8', + env={**os.environ, + **neutral_locale_variables, + }, + ) + self.last_progress = Progress() + + def get_read_fd(self): + return self.proc.stdout + + def progress_read(self): + line = self.proc.stdout.readline() + if match := self._rsync_progress_re.search(line): + self.last_progress = Progress( + nbbytes=int(match.group(1)), + percent=int(match.group(2)), + speed=humanfriendly.parse_size(match.group(3), binary=True), + eta=match.group(4), + ) + return self.last_progress + + def terminate(self): + self.proc.terminate() + + def poll(self): + return self.proc.poll() + + def close(self): + self.proc.wait() + + +if __name__ == '__main__': + import sys + import contextlib + with contextlib.ExitStack() as stack: + stack.enter_context(contextlib.suppress(KeyboardInterrupt)) + runner = stack.enter_context(Download(sys.argv[1], sys.argv[2])) + while (rc := runner.poll()) is None: + print(runner.progress_read()) + print('runner ended with code:', rc) + print('test main ended') diff --git a/gamechestcli/extract.py b/gamechestcli/gamechest/runners/extract.py index 2ac7fde..e8011a7 100644 --- a/gamechestcli/extract.py +++ b/gamechestcli/gamechest/runners/extract.py @@ -1,14 +1,14 @@ -#!/usr/bin/python3 - import os import re import subprocess import humanfriendly -from structures import Progress +from ..structures import Progress +from .runnerbase import RunnerBase, neutral_locale_variables + -class Extract: +class Extract(RunnerBase): _progress_re = re.compile(r'^\s*(\S+)\s+\[([^]]+)/s\]\s+ETA\s+(\S+)\s*$') @@ -16,10 +16,8 @@ class Extract: common_parameters = dict( encoding='utf8', env={**os.environ, - **{'LC_ALL':'C.UTF-8', - 'LANG':'C.UTF-8', - 'LANGUAGE':'C.UTF-8', - }}, + **neutral_locale_variables, + }, ) self.src_size = os.stat(src).st_size self.pv_proc = subprocess.Popen( @@ -58,8 +56,7 @@ class Extract: ) self.last_progress = Progress() - def select_fd(self): - 'useful to use selectors with the process most meaningful fd' + def get_read_fd(self): return self.pv_proc.stderr def progress_read(self): @@ -67,10 +64,10 @@ class Extract: if match := self._progress_re.search(line): written_bytes = humanfriendly.parse_size(match.group(1)) self.last_progress = Progress( - written_bytes, - int(100 * written_bytes / self.src_size), - humanfriendly.parse_size(match.group(2)), - match.group(3), + nbbytes=written_bytes, + percent=int(100 * written_bytes / self.src_size), + speed=humanfriendly.parse_size(match.group(2)), + eta=match.group(3), ) return self.last_progress @@ -79,31 +76,21 @@ class Extract: proc.terminate() def poll(self): - 'returns None if not terminated, otherwise return returncode' return self.tar_proc.poll() - def wait(self, timeout=None): - self.pv_proc.wait(timeout) - self.zip_proc.wait(timeout) - return self.tar_proc.wait(timeout) - - def __enter__(self): - return self - - def __exit__(self, exc_type, value, traceback): - self.terminate() - self.wait() + def close(self): + self.pv_proc.wait() + self.zip_proc.wait() + self.tar_proc.wait() if __name__ == '__main__': import sys import contextlib with contextlib.suppress(KeyboardInterrupt): - with Extract(sys.argv[1], sys.argv[2]) as extract: - while extract.poll() is None: - progress = extract.progress_read() - print(f'{progress.bytes}b {progress.percent}% ' - f'{progress.speed}b/s {progress.eta}') - rc = extract.poll() - print(f'ended with code: {rc}') + with Extract(sys.argv[1], sys.argv[2]) as runner: + while runner.poll() is None: + print(runner.progress_read()) + rc = runner.poll() + print('ended with code:', rc) print('test main ended') diff --git a/gamechestcli/gamechest/runners/install.py b/gamechestcli/gamechest/runners/install.py new file mode 100644 index 0000000..a827e25 --- /dev/null +++ b/gamechestcli/gamechest/runners/install.py @@ -0,0 +1,42 @@ +import functools +import os + +from .runnermulti import MultiSequencialRunnerBase +from .download import Download +from .extract import Extract +from .remove import Remove + + +class Install(MultiSequencialRunnerBase): + + def __init__(self, source, dest): + filename = os.path.split(source)[1] + tmpdest = os.path.join(dest, f'{filename}.rsynctmp') + runners = [ + functools.partial(Download, source, tmpdest), + functools.partial(Extract, tmpdest, dest), + functools.partial(Remove, tmpdest), + ] + super().__init__(runners) + + +if __name__ == '__main__': + import sys + import contextlib + import time + import selectors + print('main test') + with contextlib.ExitStack() as stack: + stack.enter_context(contextlib.suppress(KeyboardInterrupt)) + runner = stack.enter_context(Install(sys.argv[1], sys.argv[2])) + selector = stack.enter_context(selectors.DefaultSelector()) + selector.register(runner.get_read_fd(), selectors.EVENT_READ) + last_progress = None + while (rc := runner.poll()) is None: + if selector.select(): + progress = runner.progress_read() + if progress != last_progress: + print(progress) + last_progress = progress + print('ended with code:', rc) + print('test main ended') diff --git a/gamechestcli/remove.py b/gamechestcli/gamechest/runners/remove.py index 00247f7..99c4247 100644 --- a/gamechestcli/remove.py +++ b/gamechestcli/gamechest/runners/remove.py @@ -1,6 +1,3 @@ -#!/usr/bin/python3 - -import io import os import re import subprocess @@ -8,20 +5,22 @@ import threading import humanfriendly -from structures import Progress +from ..structures import Progress +from .runnerbase import RunnerBase, neutral_locale_variables -class Remove: +class Remove(RunnerBase): _progress_re = re.compile(r'^\s*(\S+)\s+\[([^]]+)/s\]\s+ETA\s+(\S+)\s*$') def __init__(self, path): - self.last_progress = Progress() + self.last_progress = Progress(linemode=True) self.path = path self.rm_proc = None self.pv_proc = None self.pipe = os.pipe() - self.read_fd = io.open(self.pipe[0], 'r', encoding='utf8') + self.read_fd = open(self.pipe[0], 'r', encoding='utf8') + #self.read_fd = open(self.pipe[0], 'rb', buffering=0) self._proc_started = False self._filescount = 0 self._counting_quit_event = threading.Event() @@ -35,10 +34,8 @@ class Remove: common_parameters = dict( encoding='utf8', env={**os.environ, - **{'LC_ALL':'C.UTF-8', - 'LANG':'C.UTF-8', - 'LANGUAGE':'C.UTF-8', - }}, + **neutral_locale_variables, + }, ) self.rm_proc = subprocess.Popen( [ @@ -51,17 +48,13 @@ class Remove: '--', self.path, ], - stdout=subprocess.PIPE, - **common_parameters, - ) - #self.rm_proc = subprocess.Popen( # only for testing purposes - # [ + # [ # only for testing # 'testtools/fake-rm', # self.path, # ], - # stdout=subprocess.PIPE, - # **common_parameters, - #) + stdout=subprocess.PIPE, + **common_parameters, + ) self.pv_proc = subprocess.Popen( [ 'pv', @@ -93,61 +86,51 @@ class Remove: break # set filescount with calculated total (even then interrupted, as it # would be a better estimation than nothing). - self._filescount = total + if os.path.isfile(path): + self._filescount = 1 + else: + self._filescount = total # start next processes (if event was not set) if not event.is_set(): self._start_proc() - def select_fd(self): - 'useful to use selectors with the process most meaningful fd' + def get_read_fd(self): return self.read_fd def progress_read(self): if not self._proc_started: return self.last_progress line = self.read_fd.readline() + #line = self.read_fd.readline().decode('utf8') if match := self._progress_re.search(line): written_bytes = humanfriendly.parse_size(match.group(1)) self.last_progress = Progress( - written_bytes, - int(100 * written_bytes / self._filescount), - humanfriendly.parse_size(match.group(2)), - match.group(3), + linemode=True, + nbbytes=written_bytes, + percent=int(100 * written_bytes / self._filescount), + speed=humanfriendly.parse_size(match.group(2)), + eta=match.group(3), ) return self.last_progress def terminate(self): + self._counting_quit_event.set() if self._proc_started: for proc in (self.rm_proc, self.pv_proc): proc.terminate() - else: - self._counting_quit_event.set() def poll(self): - 'returns None if not terminated, otherwise return returncode' if not self._proc_started: return None return self.pv_proc.poll() - def wait(self, timeout=None): - if self._proc_started: - self.rm_proc.wait(timeout) - return self.pv_proc.wait(timeout) - else: - self._counting_thread.join(timeout) - return -1 - def close(self): + self._counting_thread.join() + if self._proc_started: + for proc in (self.rm_proc, self.pv_proc): + proc.wait() self.read_fd.close() - def __enter__(self): - return self - - def __exit__(self, exc_type, value, traceback): - self.terminate() - self.wait() - self.close() - if __name__ == '__main__': import sys @@ -157,14 +140,11 @@ if __name__ == '__main__': with contextlib.ExitStack() as stack: stack.enter_context(contextlib.suppress(KeyboardInterrupt)) - remove = stack.enter_context(Remove(sys.argv[1])) + runner = stack.enter_context(Remove(sys.argv[1])) selector = stack.enter_context(selectors.DefaultSelector()) - selector.register(remove.select_fd(), selectors.EVENT_READ) - while remove.poll() is None: - selector.select() - progress = remove.progress_read() - print(f'{progress.bytes}b {progress.percent}% ' - f'{progress.speed}b/s {progress.eta}') - rc = remove.poll() - print(f'ended with code: {rc}') + selector.register(runner.get_read_fd(), selectors.EVENT_READ) + while (rc := runner.poll()) is None: + if selector.select(): + print(runner.progress_read()) + print('ended with code:', rc) print('test main ended') diff --git a/gamechestcli/gamechest/runners/runnerbase.py b/gamechestcli/gamechest/runners/runnerbase.py new file mode 100644 index 0000000..ac64ebb --- /dev/null +++ b/gamechestcli/gamechest/runners/runnerbase.py @@ -0,0 +1,44 @@ +from abc import ABCMeta, abstractmethod + + +class RunnerBase(metaclass=ABCMeta): + + @abstractmethod + def get_read_fd(self): + 'fd to select for read evts to know when to progress_read nonblockingly' + raise NotImplementedError + + @abstractmethod + def progress_read(self): + 'parse the fd given by get_read_fd and returns current progress' + raise NotImplementedError + + @abstractmethod + def terminate(self): + '''signal runner(s) to cancel immediately the operation, must + be idempotent if already terminated (not running)''' + raise NotImplementedError + + @abstractmethod + def poll(self): + 'returns None if still running, otherwise return returncode' + raise NotImplementedError + + def close(self): + 'frees resources, clean/unzombify/gc/close-fds, can block if running' + + def __enter__(self): + 'returns self' + return self + + def __exit__(self, exc_type, value, traceback): + 'terminate and wait garbage-collect' + self.terminate() + self.close() + + +neutral_locale_variables = { + 'LC_ALL':'C.UTF-8', + 'LANG':'C.UTF-8', + 'LANGUAGE':'C.UTF-8', +} diff --git a/gamechestcli/gamechest/runners/runnermulti.py b/gamechestcli/gamechest/runners/runnermulti.py new file mode 100644 index 0000000..2a5e17c --- /dev/null +++ b/gamechestcli/gamechest/runners/runnermulti.py @@ -0,0 +1,91 @@ +import contextlib +import os +import selectors +import threading +from functools import partial + +from ..structures import Progress +from .runnerbase import RunnerBase + + +def _create_pipe(): + pipe_rd, pipe_wd = os.pipe() + return open(pipe_rd, 'rb', buffering=0), open(pipe_wd, 'wb', buffering=0) + + +class MultiSequencialRunnerBase(RunnerBase): + + def __init__(self, runners): + super().__init__() + self.runners = runners + self._pipe_rd, self._pipe_wd = _create_pipe() + self._pipesig_rd, self._pipesig_wd = _create_pipe() + self._thread = threading.Thread(target=self._thread_target) + self._thread.start() + self._last_progress = Progress() + self._last_rc = -1 + + def _runner_run(self, + runner_callable, + step_index, + runners_count): + with contextlib.ExitStack() as stack: + runner = stack.enter_context(runner_callable()) + selector = stack.enter_context(selectors.DefaultSelector()) + selector.register(self._pipesig_rd, selectors.EVENT_READ) + selector.register(runner.get_read_fd(), selectors.EVENT_READ) + while (rc := runner.poll()) is None: + for key, events in selector.select(): + if key.fileobj is self._pipesig_rd: + self._pipesig_rd.read(1) + return -1 + progress = runner.progress_read() + self._last_progress = progress._replace( + step=step_index, + steps_count=runners_count, + ) + self._pipe_wd.write(b'p') # (p)rogress, could be anything + return rc + + def _thread_target(self): + runners_count = len(self.runners) + with contextlib.ExitStack() as stack: + for step_index, runner_callable in enumerate(self.runners): + self._last_rc = self._runner_run(runner_callable, step_index, + runners_count) + if self._last_rc != 0: + break + # closing writing end of a pipe, allows select on reading-end to + # immediately return and reading on reading-end will returns EOF (or + # empty string on python). + self._pipe_wd.close() + + def get_read_fd(self): + return self._pipe_rd + + def progress_read(self): + # read: discard byte used to signal progress, and ignore if EOF. + self._pipe_rd.read(1) + return self._last_progress + + def terminate(self): + # ignore ValueError, can arrive on closed _pipesig_wd if terminate is + # called multiple times. + with contextlib.suppress(ValueError): + self._pipesig_wd.write(b't') # t for terminate, could be anything + # close to avoid filling the pipe buffer uselessly if called multiple + # times, since nothing will read next sent bytes. + self._pipesig_wd.close() + + def poll(self): + return None if self._thread.is_alive() else self._last_rc + + def close(self): + self._thread.join() + for fd in ( + self._pipesig_rd, + self._pipesig_wd, + self._pipe_rd, + self._pipe_wd, + ): + fd.close() diff --git a/gamechestcli/gamechest/statusdb.py b/gamechestcli/gamechest/statusdb.py new file mode 100644 index 0000000..a010206 --- /dev/null +++ b/gamechestcli/gamechest/statusdb.py @@ -0,0 +1,93 @@ +import sqlite3 + +from xdg import xdg_state_home + +from . import consts + + +class StatusDB: + + def __init__(self): + db_path_dir = xdg_state_home() / consts.XDG_RESOURCE_NAME + db_path_dir.mkdir(parents=True, exist_ok=True) + db_path = db_path_dir / consts.STATUS_DB_NAME + self.conn = sqlite3.connect(db_path) + with self.conn: + self.conn.execute(''' + CREATE TABLE IF NOT EXISTS status ( + game_id text, + installed bool, + version_installed text + ) + ''') + + def close(self): + self.conn.close() + + def is_installed(self, game_info): + row = ( + self.conn + .execute('SELECT installed FROM status WHERE game_id = ?', + (game_info['id'], )) + .fetchone() + ) + if row is None: + return False + return bool(row[0]) + + def set_installed(self, game_info, installed=True): + cursor = self.conn.cursor() + row = ( + cursor + .execute('SELECT installed FROM status WHERE game_id = ?', + (game_info['id'], )) + .fetchone() + ) + with cursor: + if row is None: + cursor.execute(''' + INSERT INTO status + (game_id, installed, version_installed) + VALUES (?, ?, ?) + ''', ( + game_info['id'], + installed, + game_info['version'], + )) + else: + cursor.execute(''' + UPDATE status SET + installed = ?, + version_installed = ? + WHERE game_id = ? + ''', ( + installed, + game_info['version'], + game_info['id'], + )) + + def get_installed_game_name(self, game_id): + row = ( + self.conn + .execute('SELECT version_installed FROM status WHERE game_id = ?', + (game_id, )) + .fetchone() + ) + if row is None: + #return False + raise ValueError('Game not found') + version_installed = row[0] + return f'{game_id}_v{version}' + + def unset_installed(self, game_info): + return self.set_installed(game_info, installed=False) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +if __name__ == "__main__": + status_db = StatusDB() diff --git a/gamechestcli/gamechest/structures.py b/gamechestcli/gamechest/structures.py new file mode 100644 index 0000000..35a8861 --- /dev/null +++ b/gamechestcli/gamechest/structures.py @@ -0,0 +1,38 @@ +from typing import NamedTuple + + +class Progress(NamedTuple): + ''' + linemode(False) + Change number of bytes to number of lines in nbbytes field. Same is + true for speed field which in this case will be expressed as lines per + second. + + percent(0) + Current percentage for the current step. + + step(0) + Current step index. + + steps_count(1) + Number of steps. + + nbbytes(0) + Number of bytes currently processed in this step. + + speed(0) + Current number of bytes per second processed. + + eta("inifinite") + Current estimation for the end of this step. + ''' + + linemode: bool = False + + percent: int = 0 + step: int = 0 + steps_count: int = 1 + + nbbytes: int = 0 + speed: int = 0 + eta: str = "infinite" diff --git a/gamechestcli/multitest.py b/gamechestcli/multitest.py deleted file mode 100644 index 8023a4e..0000000 --- a/gamechestcli/multitest.py +++ /dev/null @@ -1,10 +0,0 @@ - -class Multi: - - def __init__(self): - pass - - - def read_progress(self): - while True: - yield diff --git a/gamechestcli/requirements-dev.txt b/gamechestcli/requirements-dev.txt new file mode 100644 index 0000000..fe96c42 --- /dev/null +++ b/gamechestcli/requirements-dev.txt @@ -0,0 +1,2 @@ +ipython +ipdb diff --git a/gamechestcli/requirements.txt b/gamechestcli/requirements.txt index f5368c4..576e0b3 100644 --- a/gamechestcli/requirements.txt +++ b/gamechestcli/requirements.txt @@ -1 +1,4 @@ humanfriendly +pyyaml +xdg +rich diff --git a/gamechestcli/rsync.py b/gamechestcli/rsync.py deleted file mode 100644 index 37330f9..0000000 --- a/gamechestcli/rsync.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/python3 - -import os -import re -import subprocess - -import humanfriendly - -from .structures import Progress - -class Rsync: - - _rsync_progress_re = re.compile(r'^\s*(\S+)\s+(\d+)%\s+(\S+)\s+(\S+)\s+$') - - def __init__(self, src, dst): - self.proc = subprocess.Popen( - [ - 'rsync', - '--partial', - # not human readable, easier to parse (but speed still appears - # in human form). - '--no-h', - '--info=progress2', - src, - dst, - ], - stdout=subprocess.PIPE, - #stderr=subprocess.DEVNULL, - encoding='utf8', - env={**os.environ, - **{'LC_ALL':'C.UTF-8', - 'LANG':'C.UTF-8', - 'LANGUAGE':'C.UTF-8', - }}, - ) - self.last_progress = Progress() - - def select_fd(self): - 'useful to use selectors with the process stdout file descriptor' - return self.proc.stdout - - def progress_read(self): - line = self.proc.stdout.readline() - if match := self._rsync_progress_re.search(line): - self.last_progress = Progress( - int(match.group(1)), - int(match.group(2)), - humanfriendly.parse_size(match.group(3), binary=True), - match.group(4), - ) - return self.last_progress - - def terminate(self): - self.proc.terminate() - - def poll(self): - 'returns None if not terminated, otherwise return returncode' - return self.proc.poll() - - def wait(self, timeout=None): - return self.proc.wait(timeout) - - def __enter__(self): - return self - - def __exit__(self, exc_type, value, traceback): - self.terminate() - self.wait() - - -if __name__ == '__main__': - import sys - import contextlib - with contextlib.suppress(KeyboardInterrupt): - with Rsync(sys.argv[1], sys.argv[2]) as rsync: - while rsync.poll() is None: - progress = rsync.progress_read() - print(f'{progress.bytes}b {progress.percent}% ' - f'{progress.speed}b/s {progress.eta}') - rc = rsync.poll() - print(f'rsync ended with code: {rc}') - print('Rsync test main ended') diff --git a/gamechestcli/structures.py b/gamechestcli/structures.py deleted file mode 100644 index 22b0e7e..0000000 --- a/gamechestcli/structures.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/python3 - -import collections - -Progress = collections.namedtuple( - 'CurrentProgress', - 'bytes percent speed eta', - defaults=[0, 0, 0, 'infinite']) - |