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 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]
@@ -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
@@ -0,0 +1,6 @@
1
+ class ConfigurationError(Exception):
2
+ pass
3
+
4
+
5
+ class DecoratorIncompatibilityError(ConfigurationError):
6
+ pass
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
@@ -0,0 +1,5 @@
1
+ import math
2
+
3
+ FLOAT_EPSILON = 1e-09
4
+
5
+ float_equals = math.isclose
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)