iripau 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iripau/__init__.py ADDED
File without changes
iripau/executable.py ADDED
@@ -0,0 +1,108 @@
1
+ """
2
+ Execute commands as Python functions
3
+ """
4
+
5
+ from shlex import split
6
+ from random import choice
7
+ from itertools import chain
8
+
9
+ from iripau.command import host_run
10
+
11
+
12
+ class Command:
13
+ """ Run an executable command as a Python callable """
14
+
15
+ def __init__(self, parent, command):
16
+ self._parent = parent
17
+ self._command = parent._mk_command(command)
18
+ self._mk_command = parent._mk_command
19
+
20
+ def __getattr__(self, command):
21
+ child = Command(self, command)
22
+ setattr(self, command, child)
23
+ return child
24
+
25
+ def __call__(self, *args, **kwargs):
26
+ return self._parent(self._command, *args, **kwargs)
27
+
28
+
29
+ def make_command(command):
30
+ return command.replace("_", "-")
31
+
32
+
33
+ def make_option(option):
34
+ return "--" + option.replace("_", "-"),
35
+
36
+
37
+ class Executable:
38
+ """ Run an executable as a Python callable """
39
+
40
+ def __init__(
41
+ self, executable, make_command=make_command, make_option=make_option,
42
+ alias=None, run_args_prefix="_", run_function=None, **kwargs
43
+ ):
44
+ self._run = run_function or host_run
45
+ self._exe = split(executable) if isinstance(executable, str) else executable
46
+ self._alias = split(alias) if isinstance(alias, str) else alias
47
+ self._kwargs = kwargs
48
+
49
+ self._prefix = run_args_prefix
50
+ self._mk_option = make_option
51
+ self._mk_command = make_command
52
+
53
+ def __getattr__(self, command):
54
+ child = Command(self, command)
55
+ setattr(self, command, child)
56
+ return child
57
+
58
+ def __call__(self, *args, **kwargs):
59
+ optionals = chain.from_iterable(
60
+ self._make_arg(self._mk_option(key), value)
61
+ for key, value in kwargs.items()
62
+ if not key.startswith(self._prefix)
63
+ )
64
+
65
+ positionals = list(map(str, args))
66
+ optionals = list(optionals)
67
+
68
+ kwargs = {
69
+ key[len(self._prefix):]: value
70
+ for key, value in kwargs.items()
71
+ if key.startswith(self._prefix)
72
+ }
73
+
74
+ if self._alias:
75
+ kwargs.setdefault("alias", self._alias + positionals + optionals)
76
+
77
+ cmd = self._exe + positionals + optionals
78
+ return self._run(cmd, **self._kwargs, **kwargs)
79
+
80
+ @staticmethod
81
+ def _is_iterable(value):
82
+ if isinstance(value, (str, bytes)):
83
+ return False
84
+ return hasattr(value, "__iter__")
85
+
86
+ @classmethod
87
+ def _make_arg(cls, options, value=None):
88
+ """ Return a list of tokens. Randomly choose a short or long option.
89
+ If a 'value' is given it's appended appropriately.
90
+ If 'value' is iterable, the option will be repeated for each item.
91
+ """
92
+ if cls._is_iterable(value):
93
+ return chain.from_iterable(
94
+ cls._make_arg(options, item)
95
+ for item in value
96
+ )
97
+
98
+ if value in {None, False}:
99
+ return []
100
+
101
+ option = choice(options)
102
+ if value is True:
103
+ return [option]
104
+
105
+ value = str(value)
106
+ if option.startswith("--"):
107
+ return [option + "=" + value]
108
+ return [option, value]
iripau/functools.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ Function utilities
3
+ """
4
+
5
+ import sys
6
+ import uuid
7
+ import operator
8
+
9
+ from functools import wraps
10
+ from inspect import getsource, isgeneratorfunction
11
+ from time import time, sleep
12
+
13
+
14
+ def wait_for(
15
+ condition, *args,
16
+ _timeout=None, _outcome=True, _poll_time=10, _stop_condition=None, **kwargs
17
+ ):
18
+ last = time()
19
+ end = _timeout and last + _timeout
20
+ operation = operator.not_ if _outcome else operator.truth
21
+ while operation(condition(*args, **kwargs)):
22
+ if _stop_condition and _stop_condition():
23
+ message = "No reason to keep waiting since the following condition was met:\n{0}"
24
+ raise InterruptedError(message.format(getsource(_stop_condition)))
25
+ now = time()
26
+ if end and now > end:
27
+ message = "The following condition was not {0} after {1} seconds: \n{2}"
28
+ raise TimeoutError(
29
+ message.format(_outcome, _timeout, getsource(condition))
30
+ )
31
+ sleep_time = max(0, _poll_time - (now - last))
32
+ sleep(sleep_time)
33
+ last = now + sleep_time
34
+
35
+
36
+ def retry(tries, exceptions=Exception, retry_condition=None, backoff_time=0):
37
+ """
38
+ Call the decorated function until it succeeds.
39
+
40
+ It will only retry if the expected exceptions are risen and the condition
41
+ is fulfilled. To verify the condition is fulfilled, it will be called
42
+ with the caught exception.
43
+
44
+ The decorated function can have two parts divided by 'yield', in that
45
+ case the expected exceptions will only be caught in the second part.
46
+ """
47
+ def decorator(function):
48
+
49
+ @wraps(function)
50
+ def helper(*args, **kwargs):
51
+ yield
52
+ return function(*args, **kwargs)
53
+
54
+ f = function if isgeneratorfunction(function) else helper
55
+
56
+ @wraps(function)
57
+ def wrapper(*args, **kwargs):
58
+ t = tries
59
+ while t > 0:
60
+ gen = f(*args, **kwargs)
61
+ next(gen)
62
+ try:
63
+ next(gen)
64
+ except StopIteration as e:
65
+ return e.value
66
+ except exceptions as e:
67
+ if retry_condition is None or retry_condition(e):
68
+ if t == 1:
69
+ raise
70
+ t = t - 1
71
+ sleep(backoff_time)
72
+ else:
73
+ raise
74
+ return wrapper
75
+ return decorator
76
+
77
+
78
+ def globalize(function):
79
+ """
80
+ Make function globally available in the module it was defined so it can
81
+ be serialized. Useful when calling local functions with multiprocessing.
82
+ """
83
+ @wraps(function)
84
+ def wrapper(*args, **kwargs):
85
+ return function(*args, **kwargs)
86
+
87
+ wrapper.__name__ = wrapper.__qualname__ = uuid.uuid4().hex
88
+ setattr(sys.modules[function.__module__], wrapper.__name__, wrapper)
89
+ return wrapper
iripau/logging.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ Logging utilities
3
+ """
4
+
5
+ import os
6
+ import re
7
+ import logging
8
+ import threading
9
+
10
+ from collections import OrderedDict
11
+
12
+
13
+ class LoggerFile:
14
+ """ File that logs every line written to it """
15
+
16
+ def __new__(cls, logger, level):
17
+ r, w = os.pipe()
18
+ read_file = os.fdopen(r, "r")
19
+ write_file = os.fdopen(w, "w")
20
+
21
+ thread = threading.Thread(
22
+ target=cls.log,
23
+ args=(read_file, logger, level),
24
+ name=threading.current_thread().name
25
+ )
26
+
27
+ # Close write_file when the thread joins
28
+ cls.patch(thread, "join", write_file.close)
29
+
30
+ # Join the thread when the file is closed
31
+ write_file.close = thread.join
32
+
33
+ thread.start()
34
+ return write_file
35
+
36
+ @staticmethod
37
+ def log(read_file, logger, level):
38
+ with read_file:
39
+ for line in read_file:
40
+ logger.log(level, line.splitlines()[0])
41
+
42
+ @staticmethod
43
+ def patch(instance, function_name, callback):
44
+ original_function = getattr(instance, function_name)
45
+
46
+ def new_function():
47
+ callback()
48
+ original_function()
49
+ setattr(instance, function_name, new_function)
50
+
51
+
52
+ class SimpleThreadNameFormatter(logging.Formatter):
53
+ """ The same logging.Formatter but threadName is just the first token after
54
+ splitting it: record.threadName.split(maxsplit=1)[0]
55
+ """
56
+
57
+ def format(self, record):
58
+ if record.threadName:
59
+ record.threadName = record.threadName.split(maxsplit=1)[0]
60
+ return super().format(record)
61
+
62
+
63
+ def group_log_lines(lines, thread_id_regex, main_thread_id="MainThread"):
64
+ """ For a log file containing entries from several threads, group the lines
65
+ so that the lines coming from the same thread are contiguous, preserving
66
+ order within the group. Logs coming from MainThread will not be grouped.
67
+ """
68
+ lines_map = OrderedDict()
69
+ for i, line in enumerate(lines):
70
+ match = re.match(thread_id_regex, line)
71
+ if not match:
72
+ raise ValueError(f"Invalid log line {i}: '{line}'")
73
+
74
+ thread = match.groups()[0]
75
+ if main_thread_id == thread:
76
+ for thread_lines in lines_map.values():
77
+ for thread_line in thread_lines:
78
+ yield thread_line
79
+ yield line
80
+ lines_map.clear()
81
+ else:
82
+ lines_map[thread] = lines_map.get(thread, []) + [line]
83
+
84
+ for thread_lines in lines_map.values():
85
+ for thread_line in thread_lines:
86
+ yield thread_line
iripau/random.py ADDED
@@ -0,0 +1,38 @@
1
+ """
2
+ Handle random data
3
+ """
4
+
5
+ import math
6
+ import random
7
+ import string
8
+
9
+
10
+ def one(items):
11
+ """ Return a new list containing only one element from items """
12
+ return [random.choice(items)]
13
+
14
+
15
+ def some(items, percentage=50, at_least=2, at_most=None):
16
+ """ Return a new list containing some elements from items.
17
+ Ordinality is preserved as much as possible.
18
+ """
19
+ count = math.ceil(percentage * len(items) / 100)
20
+ if at_least and at_least > count:
21
+ count = at_least
22
+ if at_most and at_most < count:
23
+ count = at_most
24
+ return sorted(random.sample(items, count), key=items.index)
25
+
26
+
27
+ def shuffled(items):
28
+ """ Return a new list containing all of the elements from items but in a
29
+ different order.
30
+ """
31
+ return random.sample(items, len(items))
32
+
33
+
34
+ def random_string(length, chars=string.ascii_letters):
35
+ """ Return a string of the desired length containing randomly chosen
36
+ characters from chars.
37
+ """
38
+ return "".join(random.choice(chars) for _ in range(length))
iripau/shutil.py ADDED
@@ -0,0 +1,111 @@
1
+ """
2
+ Shell utilities
3
+ """
4
+
5
+ import os
6
+ import errno
7
+ import select
8
+ import shutil
9
+ import contextlib
10
+
11
+ from iripau.functools import wait_for
12
+
13
+
14
+ class FileLock:
15
+ poll_time = 0.05
16
+
17
+ def __init__(self, file_name, timeout=None):
18
+ self.file_name = file_name
19
+ self.timeout = timeout
20
+ self.acquired = False
21
+
22
+ def __enter__(self):
23
+ self.acquire(self.timeout)
24
+ return self
25
+
26
+ def __exit__(self, type, value, traceback):
27
+ self.release()
28
+
29
+ def __del__(self):
30
+ self.release()
31
+
32
+ def _lock_file_created(self):
33
+ """ Lock file could be created by us """
34
+ try:
35
+ self.fd = os.open(
36
+ self.file_name,
37
+ os.O_CREAT | os.O_EXCL | os.O_RDWR
38
+ )
39
+ except OSError as e:
40
+ if e.errno != errno.EEXIST:
41
+ raise
42
+ else:
43
+ return True
44
+ return False
45
+
46
+ def acquire(self, timeout=None):
47
+ if not self.acquired:
48
+ wait_for(
49
+ self._lock_file_created,
50
+ _timeout=timeout or self.timeout,
51
+ _poll_time=self.poll_time
52
+ )
53
+ self.acquired = True
54
+
55
+ def release(self):
56
+ if self.acquired:
57
+ os.close(self.fd)
58
+ os.unlink(self.file_name)
59
+ self.acquired = False
60
+
61
+
62
+ def create_file(file_name, content=""):
63
+ mode = "wb" if bytes == type(content) else "w"
64
+ with open(file_name, mode) as f:
65
+ f.write(content)
66
+
67
+
68
+ def read_file(file_name, binary=False):
69
+ mode = "rb" if binary else "r"
70
+ with open(file_name, mode) as f:
71
+ return f.read()
72
+
73
+
74
+ def remove_file(file_name):
75
+ with contextlib.suppress(FileNotFoundError):
76
+ os.remove(file_name)
77
+
78
+
79
+ def remove_tree(root):
80
+ with contextlib.suppress(FileNotFoundError):
81
+ if os.path.isdir(root) and not os.path.islink(root):
82
+ shutil.rmtree(root)
83
+ else:
84
+ os.remove(root)
85
+
86
+
87
+ @contextlib.contextmanager
88
+ def file_created(file_name, content=""):
89
+ create_file(file_name, content)
90
+ try:
91
+ yield file_name
92
+ finally:
93
+ os.remove(file_name)
94
+
95
+
96
+ def wait_for_file(file_obj, *args, **kwargs):
97
+ poll_obj = select.poll()
98
+ poll_obj.register(file_obj, select.POLLIN)
99
+ wait_for(lambda: poll_obj.poll(0), *args, **kwargs)
100
+
101
+
102
+ def _rotate(root, ext, seq=0):
103
+ old = seq and f"{root}.{seq}{ext}" or root + ext
104
+ if os.path.exists(old):
105
+ seq += 1
106
+ _rotate(root, ext, seq)
107
+ os.rename(old, f"{root}.{seq}{ext}")
108
+
109
+
110
+ def rotate(path, seq=0):
111
+ _rotate(*os.path.splitext(path))