d8s-utility 0.9.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.
@@ -0,0 +1,5 @@
1
+ from .utility import *
2
+
3
+ __version__ = "0.9.0"
4
+ __author__ = """Floyd Hightower"""
5
+ __email__ = "floyd.hightower27@gmail.com"
d8s_utility/utility.py ADDED
@@ -0,0 +1,353 @@
1
+ """This is a collection of functions that really don't belong anywhere else."""
2
+
3
+ import functools
4
+ from typing import Any, Dict, Iterable, Set, Union
5
+
6
+ from .utility_temp_utils import listify_first_arg
7
+
8
+ StrOrNumberType = Union[str, int, float]
9
+
10
+
11
+ def copy_first_arg(func):
12
+ """Decorator to make a copy of the first argument and pass into the func."""
13
+
14
+ @functools.wraps(func)
15
+ def wrapper(*args, **kwargs):
16
+ import copy
17
+
18
+ first_arg = args[0]
19
+ other_args = args[1:]
20
+ try:
21
+ first_arg_copy = copy.deepcopy(first_arg)
22
+ # a RecursionError can occur when trying to do a deep copy on objects of certain classes...
23
+ # (e.g. beautifulsoup objects)...
24
+ # see: https://github.com/biopython/biopython/issues/787, https://bugs.python.org/issue5508, and...
25
+ # https://github.com/cloudtools/troposphere/issues/648
26
+ except RecursionError:
27
+ message = "Performing a deep copy on the first arg failed; I'll just perform a shallow copy."
28
+ print(message)
29
+ first_arg_copy = copy.copy(first_arg)
30
+ return func(first_arg_copy, *other_args, **kwargs)
31
+
32
+ return wrapper
33
+
34
+
35
+ def has_more_than_one_item(thing: Any) -> bool:
36
+ """Return whether or not the given thing has a length of at least one."""
37
+ return thing and len(thing) > 1
38
+
39
+
40
+ def has_one_or_more_items(thing: Any) -> bool:
41
+ """Return whether or not the given thing has a length of at least one."""
42
+ return thing and len(thing) >= 1
43
+
44
+
45
+ def has_one_item(thing: Any) -> bool:
46
+ """Return whether or not the given thing has a length of at least one."""
47
+ return thing and len(thing) == 1
48
+
49
+
50
+ def request_or_read(path):
51
+ """If the given path is a URL, request the URL and return the content; if the path exists read the file.
52
+
53
+ Otherwise, just return the string and assume it is the input itself.
54
+ """
55
+ from d8s_file_system import file_exists, file_read
56
+ from d8s_networking import get
57
+ from d8s_urls import is_url
58
+
59
+ # TODO: improve the code below; it is all wrapped in a try-except block primarily due to...
60
+ # ValueErrors when trying to check if the file exists
61
+ try:
62
+ if is_url(path):
63
+ return get(path, process_response=True)
64
+ # TODO: do more here to make sure the path looks like a file path
65
+ elif file_exists(path):
66
+ return file_read(path)
67
+ else:
68
+ return path
69
+ except ValueError:
70
+ return path
71
+
72
+
73
+ def request_or_read_first_arg(func):
74
+ """If the first arg is a url - request the URL. If it is a file path, try to read the file.
75
+
76
+ If it is neither a URL nor file path, return the content of the first arg.
77
+ """
78
+
79
+ @functools.wraps(func)
80
+ def wrapper(*args, **kwargs):
81
+ first_arg = args[0]
82
+ other_args = args[1:]
83
+
84
+ new_first_arg = request_or_read(first_arg)
85
+
86
+ return func(new_first_arg, *other_args, **kwargs)
87
+
88
+ return wrapper
89
+
90
+
91
+ @listify_first_arg
92
+ def is_sorted(iterable, *, descending: bool = False) -> bool:
93
+ """Return whether or not the iterable is sorted."""
94
+ return sorted(iterable, reverse=descending) == iterable
95
+
96
+
97
+ @listify_first_arg
98
+ def first_unsorted_value(iterable, *, descending: bool = False) -> Any:
99
+ """Return the first unsorted value in the iterable."""
100
+ sorted_items = sorted(iterable, reverse=descending)
101
+ for original_item, sorted_item in zip(iterable, sorted_items):
102
+ if original_item != sorted_item:
103
+ return original_item
104
+
105
+
106
+ @listify_first_arg
107
+ @copy_first_arg
108
+ def last_unsorted_value(iterable, *, descending: bool = False) -> Any:
109
+ """Return the last unsorted value in the iterable."""
110
+ # we reverse everything so we can iterate through the iterable and return the first item that is not sorted
111
+ iterable.reverse()
112
+ descending = not descending
113
+
114
+ sorted_items = sorted(iterable, reverse=descending)
115
+ for original_item, sorted_item in zip(iterable, sorted_items):
116
+ if original_item != sorted_item:
117
+ return original_item
118
+
119
+
120
+ @listify_first_arg
121
+ def unsorted_values(iterable, *, descending: bool = False) -> Iterable[Any]:
122
+ """."""
123
+ sorted_items = sorted(iterable, reverse=descending)
124
+ for original_item, sorted_item in zip(iterable, sorted_items):
125
+ if original_item != sorted_item:
126
+ yield original_item
127
+
128
+
129
+ @listify_first_arg
130
+ def sorted_values(iterable, *, descending: bool = False) -> Iterable[Any]:
131
+ """."""
132
+ sorted_items = sorted(iterable, reverse=descending)
133
+ for original_item, sorted_item in zip(iterable, sorted_items):
134
+ if original_item == sorted_item:
135
+ yield original_item
136
+
137
+
138
+ def ignore_errors(function, *args, **kwargs):
139
+ """."""
140
+ result = None
141
+ try:
142
+ result = function(*args, **kwargs)
143
+ except: # pylint: disable=W0702 # noqa: E722
144
+ pass
145
+
146
+ return result
147
+
148
+
149
+ def zip_if_same_length(*iterables, debug_failure: bool = False):
150
+ """Zip the given iterables if they are the same length.
151
+
152
+ If they are not the same length, raise an assertion error.
153
+ """
154
+ from d8s_lists import iterables_are_same_length
155
+
156
+ if not iterables_are_same_length(*iterables, debug_failure=debug_failure):
157
+ message = "The given iterables are not the same length."
158
+ raise ValueError(message)
159
+
160
+ for i in zip(*iterables):
161
+ yield i
162
+
163
+
164
+ def unique_items(iterable_a: Any, iterable_b: Any) -> Dict[str, Set[Any]]:
165
+ """Find the values unique to iterable_a and iterable_b (relative to one another)."""
166
+ unique_items_list: Dict[str, Set[Any]] = {"a": set(), "b": set()}
167
+
168
+ set_a = set(iterable_a)
169
+ set_b = set(iterable_b)
170
+ unique_items_list["a"] = set_a.difference(set_b)
171
+ unique_items_list["b"] = set_b.difference(set_a)
172
+
173
+ return unique_items_list
174
+
175
+
176
+ def prettify(thing: Any, *args):
177
+ """."""
178
+ import pprint
179
+
180
+ p = pprint.PrettyPrinter(*args)
181
+ return p.pformat(thing)
182
+
183
+
184
+ def pretty_print(thing: Any, *args):
185
+ """."""
186
+ print(prettify(thing, *args))
187
+
188
+
189
+ def subprocess_run(command, input_=None):
190
+ """Run the given command as if it were run in a command line."""
191
+ import shlex
192
+ import subprocess
193
+
194
+ if isinstance(command, str):
195
+ command_list = shlex.split(command)
196
+ else:
197
+ command_list = command
198
+
199
+ process = subprocess.run(command_list, input=input_, universal_newlines=True, capture_output=True)
200
+ result = (process.stdout, process.stderr)
201
+ return result
202
+
203
+
204
+ def stringify_first_arg(func):
205
+ """Decorator to convert the first argument to a string."""
206
+
207
+ @functools.wraps(func)
208
+ def wrapper(*args, **kwargs):
209
+ first_arg_string = str(args[0])
210
+ other_args = args[1:]
211
+ return func(first_arg_string, *other_args, **kwargs)
212
+
213
+ return wrapper
214
+
215
+
216
+ def retry_if_no_result(wait_seconds=10):
217
+ """Decorator to call the given function and recall it if it returns nothing."""
218
+
219
+ def retry_decorator(func):
220
+ @functools.wraps(func)
221
+ def wrapper(*args, **kwargs):
222
+ import time
223
+
224
+ return_value = func(*args, **kwargs)
225
+
226
+ if return_value:
227
+ return return_value
228
+ else:
229
+ time.sleep(wait_seconds)
230
+ return func(*args, **kwargs)
231
+
232
+ return wrapper
233
+
234
+ return retry_decorator
235
+
236
+
237
+ def map_first_arg(func):
238
+ """If the first argument is a list or tuple, iterate through each item in the list and send it to the function."""
239
+
240
+ @functools.wraps(func)
241
+ def wrapper(*args, **kwargs):
242
+ iterable_arg = args[0]
243
+ other_args = args[1:]
244
+
245
+ # TODO: define these types elsewhere
246
+ if isinstance(iterable_arg, (list, set, tuple)):
247
+ results = []
248
+ # iterate through list argument sending each item to function (along with the other arguments/kwargs)
249
+ for item in iterable_arg:
250
+ results.append(func(item, *other_args, **kwargs))
251
+ return results
252
+ else:
253
+ return func(*args, **kwargs)
254
+
255
+ return wrapper
256
+
257
+
258
+ def repeat_concurrently(n: int = 10):
259
+ """Repeat the decorated function concurrently n times."""
260
+
261
+ def actual_decorator(func):
262
+ @functools.wraps(func)
263
+ def wrapper(*args, **kwargs):
264
+ import concurrent.futures
265
+
266
+ results = []
267
+
268
+ with concurrent.futures.ThreadPoolExecutor() as executor:
269
+ for __ in range(n):
270
+ function_submission = executor.submit(func, *args, **kwargs)
271
+ yield function_submission.result()
272
+
273
+ return results
274
+
275
+ return wrapper
276
+
277
+ return actual_decorator
278
+
279
+
280
+ def validate_keyword_arg_value(
281
+ keyword: str, valid_keyword_values: Iterable[Any], fail_if_keyword_not_found: bool = True
282
+ ):
283
+ """Validate that the value for the given keyword is in the list of valid_keyword_values."""
284
+
285
+ def actual_decorator(func):
286
+ @functools.wraps(func)
287
+ def wrapper(*args, **kwargs):
288
+ keyword_exists = keyword in kwargs
289
+ if not keyword_exists and fail_if_keyword_not_found:
290
+ message = f'No keyword "{keyword}".'
291
+ raise ValueError(message)
292
+ elif keyword_exists and kwargs[keyword] not in valid_keyword_values:
293
+ message = (
294
+ f'The value of the "{keyword}" keyword argument is not valid '
295
+ + f"(valid values are: {valid_keyword_values})."
296
+ )
297
+ raise ValueError(message)
298
+
299
+ return func(*args, **kwargs)
300
+
301
+ return wrapper
302
+
303
+ return actual_decorator
304
+
305
+
306
+ def validate_arg_value(arg_index: StrOrNumberType, valid_values: Iterable[Any]):
307
+ """Validate that the value of the argument at the given arg_index is in the list of valid_values."""
308
+
309
+ def actual_decorator(func):
310
+ @functools.wraps(func)
311
+ def wrapper(*args, **kwargs):
312
+ arg_index_int = int(arg_index)
313
+
314
+ try:
315
+ arg_value = args[arg_index_int]
316
+ except IndexError as e:
317
+ message = f"No argument at index {arg_index_int}."
318
+ raise ValueError(message) from e
319
+
320
+ if arg_value not in valid_values:
321
+ message = (
322
+ f'The value of the argument at index {arg_index} (whose value is "{arg_value}") '
323
+ + "is not valid (valid values are: {valid_values})."
324
+ )
325
+ raise ValueError(message)
326
+
327
+ return func(*args, **kwargs)
328
+
329
+ return wrapper
330
+
331
+ return actual_decorator
332
+
333
+
334
+ def wait_and_retry_on_failure(wait_seconds=10):
335
+ """Try to call the given function.
336
+
337
+ If there is an exception thrown by the function, wait for wait_seconds and try again.
338
+ """
339
+
340
+ def retry_decorator(func):
341
+ @functools.wraps(func)
342
+ def wrapper(*args, **kwargs):
343
+ import time
344
+
345
+ try:
346
+ return func(*args, **kwargs)
347
+ except: # pylint: disable=W0702 # noqa: E722
348
+ time.sleep(wait_seconds)
349
+ return func(*args, **kwargs)
350
+
351
+ return wrapper
352
+
353
+ return retry_decorator
@@ -0,0 +1,16 @@
1
+ import functools
2
+
3
+
4
+ def listify_first_arg(func):
5
+ """Make sure the first argument is a list... if it is not, listify it."""
6
+
7
+ @functools.wraps(func)
8
+ def wrapper(*args, **kwargs):
9
+ first_arg = args[0]
10
+ other_args = args[1:]
11
+ if not isinstance(first_arg, list):
12
+ first_arg = list(first_arg)
13
+
14
+ return func(first_arg, *other_args, **kwargs)
15
+
16
+ return wrapper
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: d8s_utility
3
+ Version: 0.9.0
4
+ Summary: Democritus functions for working with utility functions.
5
+ Project-URL: Source, https://github.com/democritus-project/d8s-utility
6
+ Project-URL: Issues, https://github.com/democritus-project/d8s-utility/issues
7
+ Author: Floyd Hightower
8
+ License: GNU Lesser General Public License v3
9
+ License-File: COPYING
10
+ License-File: COPYING.LESSER
11
+ Keywords: democritus,python,utilities,utility,utility-functions,utility-functions-utility,utils
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: d8s-file-system<1.0,>=0.10.0
24
+ Requires-Dist: d8s-lists<1.0,>=0.8.0
25
+ Requires-Dist: d8s-networking<1.0,>=0.4.2
26
+ Requires-Dist: d8s-urls<1.0,>=0.6.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Democritus Utility
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/d8s-utility.svg)](https://pypi.python.org/pypi/d8s-utility)
32
+ [![CI](https://github.com/democritus-project/d8s-utility/workflows/CI/badge.svg)](https://github.com/democritus-project/d8s-utility/actions)
33
+ [![Lint](https://github.com/democritus-project/d8s-utility/workflows/Lint/badge.svg)](https://github.com/democritus-project/d8s-utility/actions)
34
+ [![codecov](https://codecov.io/gh/democritus-project/d8s-utility/branch/main/graph/badge.svg?token=V0WOIXRGMM)](https://codecov.io/gh/democritus-project/d8s-utility)
35
+ [![The Democritus Project uses semver version 2.0.0](https://img.shields.io/badge/-semver%20v2.0.0-22bfda)](https://semver.org/spec/v2.0.0.html)
36
+ [![The Democritus Project uses ruff to format and lint code](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
37
+ [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://choosealicense.com/licenses/lgpl-3.0/)
38
+
39
+ Democritus functions<sup>[1]</sup> for working with utility functions.
40
+
41
+ [1] Democritus functions are <i>simple, effective, modular, well-tested, and well-documented</i> Python functions.
42
+
43
+ We use `d8s` (pronounced "dee-eights") as an abbreviation for `democritus` (you can read more about this [here](https://github.com/democritus-project/roadmap#what-is-d8s)).
44
+
45
+ ## Installation
46
+
47
+ ```
48
+ pip install d8s-utility
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ You import the library like:
54
+
55
+ ```python
56
+ from d8s_utility import *
57
+ ```
58
+
59
+ Once imported, you can use any of the functions listed below.
60
+
61
+ ## Functions
62
+
63
+ - ```python
64
+ def copy_first_arg(func):
65
+ """Decorator to make a copy of the first argument and pass into the func."""
66
+ ```
67
+ - ```python
68
+ def has_more_than_one_item(thing: Any) -> bool:
69
+ """Return whether or not the given thing has a length of at least one."""
70
+ ```
71
+ - ```python
72
+ def has_one_or_more_items(thing: Any) -> bool:
73
+ """Return whether or not the given thing has a length of at least one."""
74
+ ```
75
+ - ```python
76
+ def has_one_item(thing: Any) -> bool:
77
+ """Return whether or not the given thing has a length of at least one."""
78
+ ```
79
+ - ```python
80
+ def request_or_read(path):
81
+ """If the given path is a URL, request the URL and return the content; if the path exists read the file.
82
+
83
+ Otherwise, just return the string and assume it is the input itself."""
84
+ ```
85
+ - ```python
86
+ def request_or_read_first_arg(func):
87
+ """If the first arg is a url - request the URL. If it is a file path, try to read the file.
88
+
89
+ If it is neither a URL nor file path, return the content of the first arg."""
90
+ ```
91
+ - ```python
92
+ def is_sorted(iterable, *, descending: bool = False) -> bool:
93
+ """Return whether or not the iterable is sorted."""
94
+ ```
95
+ - ```python
96
+ def first_unsorted_value(iterable, *, descending: bool = False) -> Any:
97
+ """Return the first unsorted value in the iterable."""
98
+ ```
99
+ - ```python
100
+ def last_unsorted_value(iterable, *, descending: bool = False) -> Any:
101
+ """Return the last unsorted value in the iterable."""
102
+ ```
103
+ - ```python
104
+ def unsorted_values(iterable, *, descending: bool = False) -> Iterable[Any]:
105
+ """."""
106
+ ```
107
+ - ```python
108
+ def sorted_values(iterable, *, descending: bool = False) -> Iterable[Any]:
109
+ """."""
110
+ ```
111
+ - ```python
112
+ def ignore_errors(function, *args, **kwargs):
113
+ """."""
114
+ ```
115
+ - ```python
116
+ def zip_if_same_length(*iterables, debug_failure: bool = False):
117
+ """Zip the given iterables if they are the same length.
118
+
119
+ If they are not the same length, raise an assertion error."""
120
+ ```
121
+ - ```python
122
+ def unique_items(iterable_a: Any, iterable_b: Any) -> Dict[str, Set[Any]]:
123
+ """Find the values unique to iterable_a and iterable_b (relative to one another)."""
124
+ ```
125
+ - ```python
126
+ def prettify(thing: Any, *args):
127
+ """."""
128
+ ```
129
+ - ```python
130
+ def pretty_print(thing: Any, *args):
131
+ """."""
132
+ ```
133
+ - ```python
134
+ def subprocess_run(command, input_=None):
135
+ """Run the given command as if it were run in a command line."""
136
+ ```
137
+ - ```python
138
+ def stringify_first_arg(func):
139
+ """Decorator to convert the first argument to a string."""
140
+ ```
141
+ - ```python
142
+ def retry_if_no_result(wait_seconds=10):
143
+ """Decorator to call the given function and recall it if it returns nothing."""
144
+ ```
145
+ - ```python
146
+ def map_first_arg(func):
147
+ """If the first argument is a list or tuple, iterate through each item in the list and send it to the function."""
148
+ ```
149
+ - ```python
150
+ def repeat_concurrently(n: int = 10):
151
+ """Repeat the decorated function concurrently n times."""
152
+ ```
153
+ - ```python
154
+ def validate_keyword_arg_value(
155
+ keyword: str, valid_keyword_values: Iterable[Any], fail_if_keyword_not_found: bool = True
156
+ ):
157
+ """Validate that the value for the given keyword is in the list of valid_keyword_values."""
158
+ ```
159
+ - ```python
160
+ def validate_arg_value(arg_index: StrOrNumberType, valid_values: Iterable[Any]):
161
+ """Validate that the value of the argument at the given arg_index is in the list of valid_values."""
162
+ ```
163
+ - ```python
164
+ def wait_and_retry_on_failure(wait_seconds=10):
165
+ """Try to call the given function.
166
+
167
+ If there is an exception thrown by the function, wait for wait_seconds and try again."""
168
+ ```
169
+
170
+ ## Development
171
+
172
+ ๐Ÿ‘‹ &nbsp;If you want to get involved in this project, we have some short, helpful guides below:
173
+
174
+ - [contribute to this project ๐Ÿฅ‡][contributing]
175
+ - [test it ๐Ÿงช][local-dev]
176
+ - [lint it ๐Ÿงน][local-dev]
177
+ - [explore it ๐Ÿ”ญ][local-dev]
178
+
179
+ If you have any questions or there is anything we did not cover, please raise an issue and we'll be happy to help.
180
+
181
+ ## Credits
182
+
183
+ This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and Floyd Hightower's [Python project template](https://github.com/fhightower-templates/python-project-template).
184
+
185
+ [contributing]: https://github.com/democritus-project/.github/blob/main/CONTRIBUTING.md#contributing-a-pr-
186
+ [local-dev]: https://github.com/democritus-project/.github/blob/main/CONTRIBUTING.md#local-development-
@@ -0,0 +1,8 @@
1
+ d8s_utility/__init__.py,sha256=8fIpSWRC8iZikD1Qn7qaUQq-jMf1CjSwdLEUy0vfnyY,123
2
+ d8s_utility/utility.py,sha256=FSjrP3ZUsceci09Pa9Wkikq3UNJqoQYw5-BjqiGjtIw,11012
3
+ d8s_utility/utility_temp_utils.py,sha256=s63AwfDKD9bU8kBUKEM5QXQH_dtHwvFEyr7bDWwKLq8,405
4
+ d8s_utility-0.9.0.dist-info/METADATA,sha256=D2HH4eWPsN1-ImuVbVb-HxmA3Af1WUqfCMufPgyQ8WU,7534
5
+ d8s_utility-0.9.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ d8s_utility-0.9.0.dist-info/licenses/COPYING,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
7
+ d8s_utility-0.9.0.dist-info/licenses/COPYING.LESSER,sha256=z72U6pv-bQgJ_Svr4uCXnMjemsp38aSerhHEdEAOMJ4,7632
8
+ d8s_utility-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any