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 +0 -0
- iripau/executable.py +108 -0
- iripau/functools.py +89 -0
- iripau/logging.py +86 -0
- iripau/random.py +38 -0
- iripau/shutil.py +111 -0
- iripau/subprocess.py +526 -0
- iripau/threading.py +273 -0
- iripau-0.1.0.dist-info/METADATA +21 -0
- iripau-0.1.0.dist-info/RECORD +13 -0
- iripau-0.1.0.dist-info/WHEEL +5 -0
- iripau-0.1.0.dist-info/licenses/LICENSE +373 -0
- iripau-0.1.0.dist-info/top_level.txt +1 -0
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))
|