From 016938c18dcd2c7bb81d5caaf563530fa3ca154b Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Sat, 28 Sep 2019 02:29:27 +0200 Subject: [PATCH] Import --- .gitignore | 119 +----------------------------- PKGBUILD | 27 +++++++ README.md => README | 0 dtk/__init__.py | 5 ++ dtk/argv.py | 176 ++++++++++++++++++++++++++++++++++++++++++++ dtk/cfg.py | 67 +++++++++++++++++ setup.py | 15 ++++ 7 files changed, 293 insertions(+), 116 deletions(-) create mode 100644 PKGBUILD rename README.md => README (100%) create mode 100644 dtk/__init__.py create mode 100644 dtk/argv.py create mode 100644 dtk/cfg.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index e61bca2..fb24555 100644 --- a/.gitignore +++ b/.gitignore @@ -1,116 +1,3 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - +*.pyc +__pycache__ +/MANIFEST diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..72a3eb5 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,27 @@ +# Maintainer: Martin Sekera + +pkgname=dtk-git +pkgver=r1.0000000 +pkgrel=1 +pkgdesc="Decadic toolkit." +arch=("any") +url="https://git.decade.cz/decadic/dtk.git" +license=("GPL-3") +depends=("python-jsmin") +source=("$pkgname::git+https://git.decade.cz/decadic/dtk.git") +sha256sums=("SKIP") + +pkgver() { + cd $pkgname + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +build() { + cd $pkgname + python setup.py build +} + +package() { + cd $pkgname + python setup.py install --root="$pkgdir" --optimize=1 --skip-build +} diff --git a/README.md b/README similarity index 100% rename from README.md rename to README diff --git a/dtk/__init__.py b/dtk/__init__.py new file mode 100644 index 0000000..a25bf48 --- /dev/null +++ b/dtk/__init__.py @@ -0,0 +1,5 @@ +#! /usr/bin/env python3 +# encoding: utf-8 + +import argv +import cfg diff --git a/dtk/argv.py b/dtk/argv.py new file mode 100644 index 0000000..6b3cf30 --- /dev/null +++ b/dtk/argv.py @@ -0,0 +1,176 @@ +#! /usr/bin/env python3 +# encoding: utf-8 + +import sys + +class Action: + """ + A callable with signature: + + p_args is a list of (name, type) tuples + kv_args is a dict with {name: (type, default value), ...} + """ + + p_args = [] + kv_args = {} + + def __call__(self, cmd, **kwargs): + """ + This is passed the invoking command and all the args. + + Override this. + """ + + raise NotImplementedError + +class UnknownCommand(Exception): + def __init__(self, cmd): + super().__init__(" ".join(cmd)) + +class BadArg(Exception): + def __init__(self, name, T, raw): + super().__init__("unable to interpret %s as %s (%s)" %(raw, name, T.__name__)) + +class BadKeywordArg(Exception): + def __init__(self, name): + super().__init__("unknown keyword argument %s" %(name)) + +class IncompleteInvocation(Exception): + def __init__(self): + super().__init__("one or more mandatory arguments are missing") + +class Invocation: + """ + A simple command line parser. + + Initialized with a (nested) routing dict which maps commands to Actions. + + COMMANDS ARGS [KV_ARGS] + + COMMANDS is one or more words describing the Action to take. + + ARGS is exactly N ordered words (N depends on the selected Action). + + KV_ARGS is zero or more name=value pairs. Their meaning depends on the selected Action. + + It is an error if any token remains after parsing. + """ + + def __init__(self, routing): + self.routing = routing + + self.action = None + self.cmd = [] + self.args = {} # name: value + + def parse_argv(self, argv=None): + """ + Parses the supplied argv in order to pre-select an Action to call. + + If no argv is passed, uses sys.argv[1:]. + """ + + argv = argv or sys.argv[1:] + cursor = self.routing + consumed = 0 + + # decode the command + for arg in argv: + self.cmd.append(arg) + consumed += 1 + + if arg in cursor: + cursor = cursor[arg] + else: + raise UnknownCommand(self.cmd) + + if issubclass(type(cursor), Action): + self.action = cursor + break + + # at this point the expected layout of the remaining args is known + + if not self.action: + return + + # pre-populate args + for name, T in self.action.p_args: + self.args[name] = None + + for name, (T, default) in self.action.kv_args.items(): + self.args[name] = T(default) + + # process positional (mandatory) args + count_p_args = len(self.action.p_args) + raw_p_args = argv[consumed:][:count_p_args] + + if not len(raw_p_args) == count_p_args: + raise IncompleteInvocation + + for raw, (name, T) in zip(raw_p_args, self.action.p_args): + try: + self.args[name] = T(raw) + except: + raise BadArg(name, T, raw) + + consumed += count_p_args + + # process keyword (optional) args + raw_kv_args = argv[consumed:] + + for arg in raw_kv_args: + if "=" in arg: + aname, avalue = arg.split("=", 1) + + if aname in self.action.kv_args: + self.args[aname] = self.action.kv_args[aname][0](avalue) + else: + raise BadKeywordArg(arg) + else: + raise BadKeywordArg(arg) + + def execute(self): + self.action(self.cmd, **self.args) + +# ~ $ nicectl customer create NAME [--note=xxx] + +# ~ $ nicectl user create CUSTOMER LOGIN +# ~ generated password is printed + +# ~ $ nicectl unit list + +# ~ $ nicectl unit create CUSTOMER NAME MODEL [--serial=XXX] [--note=xxx] + +# ~ routing = { + # ~ "customer": { + # ~ "create": customer_create, + # ~ "list": customer_list + # ~ }, + # ~ "user": { + # ~ "create": user_create, + # ~ "list": user_list + # ~ }, + # ~ "unit": { + # ~ "create": unit_create, + # ~ "list": unit_list + # ~ } +# ~ } + +if __name__ == "__main__": + class TestActionHello(Action): + p_args = [("NAME", str), ("AGE", int)] + kv_args = {"speed": (float, 75)} + + def __call__(self, cmd, NAME, AGE, speed): + print("Hello %s, age %d." %(NAME, AGE)) + print("Called by %s with %s" %(cmd, speed)) + + routing = { + "person": { + "hello": TestActionHello() + } + } + + invo = Invocation(routing) + invo.parse_argv(["person", "hello", "Martin", "31", "speed=85"]) + invo.execute() diff --git a/dtk/cfg.py b/dtk/cfg.py new file mode 100644 index 0000000..5c94a29 --- /dev/null +++ b/dtk/cfg.py @@ -0,0 +1,67 @@ +#! /usr/bin/env python3 +# encoding: utf-8 + +import jsmin +import json + +class Config: + def __init__(self, source=None, readonly=False): + """ + Can be loaded from a JSON file (str path) or from a python dict. + + The JSON file can contain C++ style comments. + """ + + if type(source) == str: + with open(source) as f: + initial = json.loads(jsmin.jsmin(f.read())) + elif source is None: + initial = {} + else: + initial = source + + assert type(initial) == dict + object.__setattr__(self, "raw", initial) + object.__setattr__(self, "readonly", readonly) + + def keys(self): + return self.raw.keys() + + def values(self): + return self.raw.values() + + def __getitem__(self, name): + return self.raw[name] + + def __setitem__(self, name, value): + if self.readonly: + raise AttributeError("object is not writable") + + self.raw[name] = value + + def __setattr__(self, name, value): + if self.readonly: + raise AttributeError("object is not writable") + + self.raw[name] = value + + def __getattr__(self, name): + matches = [key for key in self.raw if key.replace("-", "_") == name] + + if matches: + assert len(matches) == 1 + + value = self.raw[matches[0]] + + if type(value) == dict: + return Config(value) + else: + return value + else: + raise KeyError(name) + + def __contains__(self, name): + return name in self.raw + + def __repr__(self): + return "Config(%s)" %(" ".join("%s=%s" %(k, "…" if type(v) == dict else v) for k, v in self.raw.items())) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c7d0393 --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +from distutils.core import setup + +setup( + name = "dtk", + version = "0.0.0", + author = "Decadic", + packages = ["dtk"], + url = "https://git.decade.cz/decadic/dtk/", + license = "LICENSE", + description = "Decadic toolkit.", + install_requires = [ + "jsmin >= 2.2.2", + "json >= 2.0.9" + ] +)