communal 0.4.5__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.
- communal/__init__.py +1 -0
- communal/attrdict.py +16 -0
- communal/binary.py +79 -0
- communal/casing.py +23 -0
- communal/client.py +14 -0
- communal/collections.py +15 -0
- communal/dates.py +26 -0
- communal/download.py +36 -0
- communal/encoding.py +34 -0
- communal/enum.py +32 -0
- communal/exceptions.py +6 -0
- communal/files.py +42 -0
- communal/floats.py +5 -0
- communal/functions.py +349 -0
- communal/getter.py +18 -0
- communal/guids.py +11 -0
- communal/hashing.py +25 -0
- communal/http.py +31 -0
- communal/inflections.py +7 -0
- communal/inheritance.py +20 -0
- communal/ips.py +64 -0
- communal/iterables.py +226 -0
- communal/memory.py +11 -0
- communal/mixins.py +19 -0
- communal/nesting.py +277 -0
- communal/nulls.py +32 -0
- communal/properties.py +86 -0
- communal/sets.py +8 -0
- communal/sizes.py +7 -0
- communal/submodules.py +27 -0
- communal/time_periods.py +7 -0
- communal/timezone.py +15 -0
- communal/tracebacks.py +6 -0
- communal/zip_archive.py +56 -0
- communal-0.4.5.dist-info/METADATA +34 -0
- communal-0.4.5.dist-info/RECORD +38 -0
- communal-0.4.5.dist-info/WHEEL +4 -0
- communal-0.4.5.dist-info/licenses/LICENSE +21 -0
communal/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.5"
|
communal/attrdict.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class AttrDict(dict):
|
|
2
|
+
"""
|
|
3
|
+
A dictionary that allows using the dot operator to get and set keys.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __getattr__(self, key):
|
|
7
|
+
return self[key]
|
|
8
|
+
|
|
9
|
+
def __setattr__(self, key, value):
|
|
10
|
+
self[key] = value
|
|
11
|
+
|
|
12
|
+
def __delattr__(self, key):
|
|
13
|
+
del self[key]
|
|
14
|
+
|
|
15
|
+
def __repr__(self):
|
|
16
|
+
return f"{self.__class__.__name__}({dict.__repr__(self)})"
|
communal/binary.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
from communal.encoding import safe_decode, safe_encode
|
|
4
|
+
from communal.functions import Pipeline
|
|
5
|
+
from communal.properties import classproperty
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def int_to_bytes(x: int, endianness="little"):
|
|
9
|
+
if not isinstance(x, int):
|
|
10
|
+
x = int(x)
|
|
11
|
+
return x.to_bytes((x.bit_length() + 7) // 8, endianness)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def int_from_bytes(x: bytes, endianness="little"):
|
|
15
|
+
return int.from_bytes(x, endianness)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BinaryEncoder(object):
|
|
19
|
+
@classmethod
|
|
20
|
+
def strip_padding(cls, s):
|
|
21
|
+
return s.rstrip(b"=")
|
|
22
|
+
|
|
23
|
+
@classproperty
|
|
24
|
+
def encode(cls):
|
|
25
|
+
return Pipeline(cls.encoder, cls.strip_padding)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def add_padding(cls, s):
|
|
29
|
+
return s if len(s) % cls.group_width == 0 else s.ljust(cls.group_width, b"=")
|
|
30
|
+
|
|
31
|
+
@classproperty
|
|
32
|
+
def decode(cls):
|
|
33
|
+
return Pipeline(cls.add_padding, cls.decoder)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def encode_str(cls, s: str):
|
|
37
|
+
return safe_decode(cls.encode(safe_encode(s)))
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def encode_int(cls, x: int, endianness: str = "little"):
|
|
41
|
+
return safe_decode(cls.encode(int_to_bytes(x, endianness=endianness)))
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def decode_str(cls, s: str):
|
|
45
|
+
return safe_decode(cls.decode(safe_encode(s)))
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def decode_int(cls, s: str, endianness: str = "little"):
|
|
49
|
+
return int_from_bytes(cls.decode(safe_encode(s)), endianness=endianness)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Base16(BinaryEncoder):
|
|
53
|
+
bits = 16
|
|
54
|
+
group_width = 2
|
|
55
|
+
encoder = base64.b16encode
|
|
56
|
+
decoder = base64.b16decode
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Base32(BinaryEncoder):
|
|
60
|
+
bits = 32
|
|
61
|
+
group_width = 8
|
|
62
|
+
encoder = base64.b32encode
|
|
63
|
+
decoder = base64.b32decode
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Base64(BinaryEncoder):
|
|
67
|
+
bits = 64
|
|
68
|
+
group_width = 8
|
|
69
|
+
encoder = base64.b64encode
|
|
70
|
+
decoder = base64.b64decode
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Base85(BinaryEncoder):
|
|
74
|
+
bits = 85
|
|
75
|
+
encoder = base64.b85encode
|
|
76
|
+
decoder = base64.b85decode
|
|
77
|
+
|
|
78
|
+
encode = encoder
|
|
79
|
+
decode = decoder
|
communal/casing.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
camel_to_snake_re_all_caps = re.compile("([A-Z\\d]+)([A-Z][a-z])")
|
|
4
|
+
camel_to_snake_re_before_word = re.compile("([a-z])([A-Z])")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def camel_to_snake_case(name):
|
|
8
|
+
name = camel_to_snake_re_all_caps.sub("\\1_\\2", name)
|
|
9
|
+
name = camel_to_snake_re_before_word.sub("\\1_\\2", name)
|
|
10
|
+
name = name.replace("-", "_")
|
|
11
|
+
return name.lower()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_camel_case(name):
|
|
15
|
+
return name.title().replace(" ", "").replace("_", "")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_title_case(name):
|
|
19
|
+
"""Convert all upper or all lower case names to title case, otherwise preserve casing."""
|
|
20
|
+
if name.islower() or name.isupper():
|
|
21
|
+
return name.title()
|
|
22
|
+
else:
|
|
23
|
+
return name
|
communal/client.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class ClientProxy(object):
|
|
2
|
+
client = None
|
|
3
|
+
|
|
4
|
+
def __getattr__(self, name):
|
|
5
|
+
return getattr(self.client, name)
|
|
6
|
+
|
|
7
|
+
def __getitem__(self, name):
|
|
8
|
+
return self.client[name]
|
|
9
|
+
|
|
10
|
+
def __setitem__(self, name, value):
|
|
11
|
+
self.client[name] = value
|
|
12
|
+
|
|
13
|
+
def __delitem__(self, name):
|
|
14
|
+
del self.client[name]
|
communal/collections.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from collections.abc import Hashable, Mapping
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def unique_list(values):
|
|
6
|
+
"""Produces a unique list while preserving input order"""
|
|
7
|
+
return list(OrderedDict.fromkeys(values))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_mapping(val):
|
|
11
|
+
return isinstance(val, Mapping)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_hashable(val):
|
|
15
|
+
return isinstance(val, Hashable)
|
communal/dates.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
from dateutil.parser import parse as _dateutil_parse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def current_timestamp():
|
|
7
|
+
return datetime.utcnow().timestamp()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def current_year():
|
|
11
|
+
return date.today().year
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
parse_datetime = _dateutil_parse
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_date(dt):
|
|
18
|
+
return _dateutil_parse(dt).date()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_datetime_ignore_tz_microseconds(d):
|
|
22
|
+
return _dateutil_parse(d, ignoretz=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_datetime_ignore_tz_seconds(d):
|
|
26
|
+
return _dateutil_parse(d, ignoretz=True).replace(microsecond=0)
|
communal/download.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import fsspec
|
|
5
|
+
import furl
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from communal.files import ensure_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def save_response_content(response, destination):
|
|
12
|
+
with fsspec.open(destination, "wb") as f:
|
|
13
|
+
shutil.copyfileobj(response.raw, f)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def download_file(url, dest, retries=3, retry_delay=5, session=requests, headers=None):
|
|
17
|
+
url = furl.furl(url)
|
|
18
|
+
|
|
19
|
+
exists = os.path.exists(dest)
|
|
20
|
+
is_dir = os.path.isdir(dest) if exists else len(os.path.splitext(dest)[1]) == 0
|
|
21
|
+
|
|
22
|
+
base_dir = dest if is_dir else os.path.dirname(dest)
|
|
23
|
+
if base_dir:
|
|
24
|
+
ensure_dir(base_dir)
|
|
25
|
+
|
|
26
|
+
dest_filename = os.path.basename(dest) if not is_dir else None
|
|
27
|
+
if not dest_filename and url.path.isfile:
|
|
28
|
+
dest_filename = url.path.segments[-1]
|
|
29
|
+
dest = os.path.join(dest, dest_filename)
|
|
30
|
+
|
|
31
|
+
with session.get(url, headers=headers, stream=True) as resp:
|
|
32
|
+
resp.raw.decode_content = True
|
|
33
|
+
resp.raise_for_status()
|
|
34
|
+
save_response_content(resp, dest)
|
|
35
|
+
|
|
36
|
+
return dest
|
communal/encoding.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
def safe_decode(
|
|
2
|
+
value, encoding="utf-8", errors="strict", text_types=str, binary_types=bytes
|
|
3
|
+
):
|
|
4
|
+
if isinstance(value, text_types):
|
|
5
|
+
return value
|
|
6
|
+
|
|
7
|
+
if isinstance(value, binary_types):
|
|
8
|
+
return value.decode(encoding, errors)
|
|
9
|
+
else:
|
|
10
|
+
return str(value)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def safe_encode(
|
|
14
|
+
value,
|
|
15
|
+
encoding="utf-8",
|
|
16
|
+
from_encoding=None,
|
|
17
|
+
errors="strict",
|
|
18
|
+
text_types=str,
|
|
19
|
+
binary_types=bytes,
|
|
20
|
+
):
|
|
21
|
+
if isinstance(value, text_types):
|
|
22
|
+
return value.encode(encoding, errors=errors)
|
|
23
|
+
elif isinstance(value, binary_types):
|
|
24
|
+
if hasattr(from_encoding, "lower"):
|
|
25
|
+
from_encoding = from_encoding.lower()
|
|
26
|
+
if hasattr(encoding, "lower"):
|
|
27
|
+
encoding = encoding.lower()
|
|
28
|
+
|
|
29
|
+
if value and from_encoding and encoding and encoding != from_encoding:
|
|
30
|
+
value = safe_decode(value, encoding=from_encoding, errors=errors)
|
|
31
|
+
return value.encode(encoding, errors)
|
|
32
|
+
return value
|
|
33
|
+
else:
|
|
34
|
+
return bytes(repr(value), encoding=encoding)
|
communal/enum.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StringEnum(str, enum.Enum):
|
|
5
|
+
def __str__(self):
|
|
6
|
+
return str(self.value)
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def display_name(cls, choice):
|
|
10
|
+
return choice.name
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def display(self):
|
|
14
|
+
return self.value.replace("_", " ").title()
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def choices(cls):
|
|
18
|
+
return [(choice.value, cls.display_name(choice)) for choice in cls]
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_value(cls, value):
|
|
22
|
+
return cls(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LabeledEnum(enum.Enum):
|
|
26
|
+
label: str
|
|
27
|
+
|
|
28
|
+
def __new__(cls, value, label):
|
|
29
|
+
obj = object.__new__(cls)
|
|
30
|
+
obj._value = value
|
|
31
|
+
obj.label = label
|
|
32
|
+
return obj
|
communal/exceptions.py
ADDED
communal/files.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import fsspec
|
|
5
|
+
|
|
6
|
+
from communal.sizes import COPY_BUFFER_SIZE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ensure_dir(d):
|
|
10
|
+
if not os.path.exists(d):
|
|
11
|
+
os.makedirs(d)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def copy_file(filename, dest, buffer_size=COPY_BUFFER_SIZE):
|
|
15
|
+
dest_kwargs = {}
|
|
16
|
+
if dest.endswith(".gz") and not filename.endswith(".gz"):
|
|
17
|
+
dest_kwargs.update(compression="gzip")
|
|
18
|
+
elif dest.endswith(".zip") and not filename.endswith(".zip"):
|
|
19
|
+
dest_kwargs.update(compression="zip")
|
|
20
|
+
|
|
21
|
+
with fsspec.open(dest, "wb", **dest_kwargs) as df:
|
|
22
|
+
with fsspec.open(filename, "rb") as sf:
|
|
23
|
+
shutil.copyfileobj(sf, df)
|
|
24
|
+
return dest
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def remove_file(filename):
|
|
28
|
+
os.unlink(filename)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class cd:
|
|
32
|
+
"""Context manager for changing the current working directory"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, path):
|
|
35
|
+
self.path = path
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
self.saved_path = os.getcwd()
|
|
39
|
+
os.chdir(self.path)
|
|
40
|
+
|
|
41
|
+
def __exit__(self, etype, value, traceback):
|
|
42
|
+
os.chdir(self.saved_path)
|
communal/floats.py
ADDED
communal/functions.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import itertools
|
|
4
|
+
import operator
|
|
5
|
+
import re
|
|
6
|
+
from collections import deque
|
|
7
|
+
|
|
8
|
+
from communal.exceptions import DecoratorIncompatibilityError
|
|
9
|
+
from communal.iterables import iterify
|
|
10
|
+
from communal.nulls import DoesNotExist
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def nop(obj):
|
|
14
|
+
return obj
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def equal_to(other):
|
|
18
|
+
return functools.partial(operator.eq, other)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def not_equal_to(other):
|
|
22
|
+
return functools.partial(operator.ne, other)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def argmin(a):
|
|
26
|
+
return min(range(len(a)), key=a.__getitem__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def argmax(a):
|
|
30
|
+
return max(range(len(a)), key=a.__getitem__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
is_none = functools.partial(operator.is_, None)
|
|
34
|
+
not_none = functools.partial(operator.is_not, None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def if_none(default):
|
|
38
|
+
def value(v):
|
|
39
|
+
return v if v is not None else default
|
|
40
|
+
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def stop_on_input_values(*vals, default=None):
|
|
45
|
+
if not vals:
|
|
46
|
+
vals = (None,)
|
|
47
|
+
|
|
48
|
+
def deco(f):
|
|
49
|
+
@functools.wraps(f)
|
|
50
|
+
def func(v):
|
|
51
|
+
return f(v) if v not in vals else default
|
|
52
|
+
|
|
53
|
+
return func
|
|
54
|
+
|
|
55
|
+
if len(vals) == 1 and callable(vals[0]):
|
|
56
|
+
f = vals[0]
|
|
57
|
+
vals = (None,)
|
|
58
|
+
return deco(f)
|
|
59
|
+
return deco
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
call_if_not_null = stop_on_input_values(None)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def stop_on_conditions(*conditions, default=None):
|
|
66
|
+
def deco(f):
|
|
67
|
+
@functools.wraps(f)
|
|
68
|
+
def func(v):
|
|
69
|
+
all_conditions_met = all((c(v) for c in conditions))
|
|
70
|
+
return f(v) if all_conditions_met else default
|
|
71
|
+
|
|
72
|
+
return func
|
|
73
|
+
|
|
74
|
+
return deco
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
true_values = {"TRUE", "T", "Y", "YES", "ON", "1"}
|
|
78
|
+
false_values = {"FALSE", "F", "N", "NO", "OFF", "0"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def str_to_bool(value: str):
|
|
82
|
+
value = (value or "").upper()
|
|
83
|
+
if value in true_values:
|
|
84
|
+
return True
|
|
85
|
+
elif value in false_values:
|
|
86
|
+
return False
|
|
87
|
+
else:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def attr_get(*keys: str, default=None):
|
|
92
|
+
def get_attrs(o):
|
|
93
|
+
return [getattr(o, k, default) for k in keys]
|
|
94
|
+
|
|
95
|
+
if len(keys) == 1:
|
|
96
|
+
key = keys[0]
|
|
97
|
+
|
|
98
|
+
def get_attr(o):
|
|
99
|
+
return getattr(o, key, default)
|
|
100
|
+
|
|
101
|
+
return get_attrs if len(keys) > 1 else get_attr
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def item_get(*keys: str, default=None):
|
|
105
|
+
def get_keys(d):
|
|
106
|
+
return [d.get(k, default) for k in keys]
|
|
107
|
+
|
|
108
|
+
if len(keys) == 1:
|
|
109
|
+
key = keys[0]
|
|
110
|
+
|
|
111
|
+
def get_key(d):
|
|
112
|
+
return d.get(key, default)
|
|
113
|
+
|
|
114
|
+
return get_keys if len(keys) > 1 else get_key
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def attr_or_key_getter(key, default=DoesNotExist):
|
|
118
|
+
def with_obj(obj):
|
|
119
|
+
val = getattr(obj, key, DoesNotExist)
|
|
120
|
+
if val is DoesNotExist:
|
|
121
|
+
try:
|
|
122
|
+
val = obj[key]
|
|
123
|
+
obj = val
|
|
124
|
+
except (IndexError, TypeError):
|
|
125
|
+
val = default
|
|
126
|
+
else:
|
|
127
|
+
obj = val
|
|
128
|
+
return val
|
|
129
|
+
|
|
130
|
+
return with_obj
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def has_all_attrs(*attrs: str):
|
|
134
|
+
def attrs_exist(o):
|
|
135
|
+
return all((hasattr(o, a) for a in attrs))
|
|
136
|
+
|
|
137
|
+
if len(attrs) == 1:
|
|
138
|
+
attr = attrs[0]
|
|
139
|
+
|
|
140
|
+
def attr_exists(o):
|
|
141
|
+
return hasattr(o, attr)
|
|
142
|
+
|
|
143
|
+
return attrs_exist if len(attrs) > 1 else attr_exists
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def all_attrs_not_none(*attrs: str):
|
|
147
|
+
def attrs_not_none(o):
|
|
148
|
+
return all((getattr(o, a, None) is not None for a in attrs))
|
|
149
|
+
|
|
150
|
+
if len(attrs) == 1:
|
|
151
|
+
attr = attrs[0]
|
|
152
|
+
|
|
153
|
+
def attr_not_none(o):
|
|
154
|
+
return getattr(o, attr, None) is not None
|
|
155
|
+
|
|
156
|
+
return attrs_not_none if len(attrs) > 1 else attr_not_none
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def has_all_keys(*keys: str):
|
|
160
|
+
def keys_exist(d):
|
|
161
|
+
return all((k in d for k in keys))
|
|
162
|
+
|
|
163
|
+
if len(keys) == 1:
|
|
164
|
+
key = keys[0]
|
|
165
|
+
|
|
166
|
+
def key_exists(d):
|
|
167
|
+
return key in d
|
|
168
|
+
|
|
169
|
+
return keys_exist if len(keys) > 1 else key_exists
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def all_keys_not_none(*keys: str):
|
|
173
|
+
def keys_not_none(d):
|
|
174
|
+
return all((d.get(k, None) is not None for k in keys))
|
|
175
|
+
|
|
176
|
+
if len(keys) == 1:
|
|
177
|
+
key = keys[0]
|
|
178
|
+
|
|
179
|
+
def key_not_none(d):
|
|
180
|
+
return d.get(key, None) is not None
|
|
181
|
+
|
|
182
|
+
return keys_not_none if len(keys) > 1 else key_not_none
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def map_values(d: dict):
|
|
186
|
+
def mapped_value(val):
|
|
187
|
+
return d.get(val, val)
|
|
188
|
+
|
|
189
|
+
return mapped_value
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Because partials are only specifying the first argument to a function,
|
|
193
|
+
# and we're effectively specifying the righthand side up front and then
|
|
194
|
+
# running the function on a bunch of inputs, we use the inverse functions
|
|
195
|
+
# e.g. less_than(5) will run x < 5 for any x. To make the arguments left-to-right,
|
|
196
|
+
# we end up calling the equivalent operator.gt(5, x), or is 5 > x
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def less_than(other):
|
|
200
|
+
return functools.partial(operator.gt, other)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def greater_than(other):
|
|
204
|
+
return functools.partial(operator.lt, other)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def less_than_or_equal_to(other):
|
|
208
|
+
return functools.partial(operator.ge, other)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def greater_than_or_equal_to(other):
|
|
212
|
+
return functools.partial(operator.le, other)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def regex_sub(pattern, repl):
|
|
216
|
+
pattern = re.compile(pattern)
|
|
217
|
+
return functools.partial(pattern.sub, repl)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Pipeline(object):
|
|
221
|
+
def __init__(self, *functions):
|
|
222
|
+
self.functions = functions
|
|
223
|
+
|
|
224
|
+
def __repr__(self):
|
|
225
|
+
return "Pipeline: [{}]".format(
|
|
226
|
+
", ".join((str(func) for func in self.functions))
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def __call__(self, val):
|
|
230
|
+
for f in self.functions:
|
|
231
|
+
val = f(val)
|
|
232
|
+
return val
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class FunctionMap(object):
|
|
236
|
+
def __init__(self, *functions, use_tuple=True):
|
|
237
|
+
self.functions = functions
|
|
238
|
+
self.use_tuple = use_tuple
|
|
239
|
+
|
|
240
|
+
def __repr__(self):
|
|
241
|
+
return f'{self.__class__.__name__}: [{", ".join((str(func) for func in self.functions))}]'
|
|
242
|
+
|
|
243
|
+
def __call__(self, *args, **kw):
|
|
244
|
+
val = [func(*args, **kw) for func in self.functions]
|
|
245
|
+
if self.use_tuple:
|
|
246
|
+
return tuple(val)
|
|
247
|
+
else:
|
|
248
|
+
return val
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class FunctionZipMap(FunctionMap):
|
|
252
|
+
def __call__(self, vals, **kw):
|
|
253
|
+
val = [func(arg, **kw) for func, arg in zip(self.functions, vals)]
|
|
254
|
+
|
|
255
|
+
if self.use_tuple:
|
|
256
|
+
return tuple(val)
|
|
257
|
+
else:
|
|
258
|
+
return val
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class FunctionStarMap(object):
|
|
262
|
+
def __init__(self, function, use_tuple=True):
|
|
263
|
+
self.function = function
|
|
264
|
+
self.use_tuple = use_tuple
|
|
265
|
+
|
|
266
|
+
def __repr__(self):
|
|
267
|
+
return f"{self.__class__.__name__}: {self.function}"
|
|
268
|
+
|
|
269
|
+
def __call__(self, *args, **kw):
|
|
270
|
+
val = list(
|
|
271
|
+
itertools.chain(*itertools.starmap(self.function, itertools.chain(*args)))
|
|
272
|
+
)
|
|
273
|
+
if self.use_tuple:
|
|
274
|
+
return tuple(val)
|
|
275
|
+
else:
|
|
276
|
+
return val
|
|
277
|
+
|
|
278
|
+
return ()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class VectorFunction(object):
|
|
282
|
+
def __init__(self, function):
|
|
283
|
+
self.function = function
|
|
284
|
+
|
|
285
|
+
def __repr__(self):
|
|
286
|
+
return f"{self.__class__.__name__}: {self.function}"
|
|
287
|
+
|
|
288
|
+
def __call__(self, values):
|
|
289
|
+
return list(map(self.function, values))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ChainedFunction(object):
|
|
293
|
+
def __init__(self, function):
|
|
294
|
+
self.function = function
|
|
295
|
+
|
|
296
|
+
def __repr__(self):
|
|
297
|
+
return f"{self.__class__.__name__}: {self.function}"
|
|
298
|
+
|
|
299
|
+
def __call__(self, values):
|
|
300
|
+
return list(itertools.chain(*(map(self.function, iterify(v)) for v in values)))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class hybridmethod(object):
|
|
304
|
+
def __init__(self, func):
|
|
305
|
+
self.func = func
|
|
306
|
+
|
|
307
|
+
def __get__(self, instance, owner):
|
|
308
|
+
self_or_cls = owner if instance is None else instance
|
|
309
|
+
|
|
310
|
+
@functools.wraps(self.func)
|
|
311
|
+
def call_hybrid(*args, **kwargs):
|
|
312
|
+
return self.func(self_or_cls, *args, **kwargs)
|
|
313
|
+
|
|
314
|
+
return call_hybrid
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_innermost_argspec(method):
|
|
318
|
+
"""When passed a method/function, drill through all layers of decorators to get the inner argspec."""
|
|
319
|
+
|
|
320
|
+
orig_method = method
|
|
321
|
+
q = deque([orig_method])
|
|
322
|
+
while q:
|
|
323
|
+
method = q.popleft()
|
|
324
|
+
if hasattr(method, "__func__"):
|
|
325
|
+
method = method.__func__
|
|
326
|
+
elif hasattr(method, "func"):
|
|
327
|
+
method = method.func
|
|
328
|
+
|
|
329
|
+
argspec = inspect.getfullargspec(method)
|
|
330
|
+
args = argspec.args
|
|
331
|
+
|
|
332
|
+
if args and args[0] in ("self", "cls"):
|
|
333
|
+
return argspec
|
|
334
|
+
if not hasattr(method, "__closure__") or method.__closure__ is None:
|
|
335
|
+
raise DecoratorIncompatibilityError(
|
|
336
|
+
"Decorator has no closure: {}".format(method)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
closure = method.__closure__
|
|
340
|
+
|
|
341
|
+
for cell in closure:
|
|
342
|
+
inner_method = cell.cell_contents
|
|
343
|
+
if inner_method is method:
|
|
344
|
+
continue
|
|
345
|
+
if not inspect.isfunction(inner_method) and not inspect.ismethod(
|
|
346
|
+
inner_method
|
|
347
|
+
):
|
|
348
|
+
continue
|
|
349
|
+
q.append(inner_method)
|