agapsys-utils 0.0.0__tar.gz
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.
- agapsys_utils-0.0.0/LICENSE +21 -0
- agapsys_utils-0.0.0/PKG-INFO +30 -0
- agapsys_utils-0.0.0/README.md +15 -0
- agapsys_utils-0.0.0/pyproject.toml +28 -0
- agapsys_utils-0.0.0/setup.cfg +4 -0
- agapsys_utils-0.0.0/src/agapsys/utils/__init__.py +2 -0
- agapsys_utils-0.0.0/src/agapsys/utils/assertions.py +104 -0
- agapsys_utils-0.0.0/src/agapsys/utils/proc.py +420 -0
- agapsys_utils-0.0.0/src/agapsys_utils.egg-info/PKG-INFO +30 -0
- agapsys_utils-0.0.0/src/agapsys_utils.egg-info/SOURCES.txt +11 -0
- agapsys_utils-0.0.0/src/agapsys_utils.egg-info/dependency_links.txt +1 -0
- agapsys_utils-0.0.0/src/agapsys_utils.egg-info/requires.txt +6 -0
- agapsys_utils-0.0.0/src/agapsys_utils.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agapsys-utils
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Common utilities
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: build; extra == "dev"
|
|
11
|
+
Requires-Dist: twine; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# agapsys-utils
|
|
17
|
+
|
|
18
|
+
This project provides general purpose utiltities that individually thet do not fit in a dedicated library.
|
|
19
|
+
|
|
20
|
+
## License
|
|
21
|
+
|
|
22
|
+
m-conf is distributed under MIT License. Please see the [LICENSE](LICENSE) file for details on copying and distribution.
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
```py
|
|
27
|
+
import agapsys.utils as utils
|
|
28
|
+
|
|
29
|
+
# That's it... use the available modules
|
|
30
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# agapsys-utils
|
|
2
|
+
|
|
3
|
+
This project provides general purpose utiltities that individually thet do not fit in a dedicated library.
|
|
4
|
+
|
|
5
|
+
## License
|
|
6
|
+
|
|
7
|
+
m-conf is distributed under MIT License. Please see the [LICENSE](LICENSE) file for details on copying and distribution.
|
|
8
|
+
|
|
9
|
+
## Basic usage
|
|
10
|
+
|
|
11
|
+
```py
|
|
12
|
+
import agapsys.utils as utils
|
|
13
|
+
|
|
14
|
+
# That's it... use the available modules
|
|
15
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agapsys-utils"
|
|
7
|
+
version = "0.0.0"
|
|
8
|
+
description = "Common utilities"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = [
|
|
16
|
+
"build",
|
|
17
|
+
"twine",
|
|
18
|
+
"pytest>=7.0",
|
|
19
|
+
"pytest-cov"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest]
|
|
26
|
+
addopts = []
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from typing import Any, Type
|
|
5
|
+
|
|
6
|
+
import builtins
|
|
7
|
+
|
|
8
|
+
def check(condition: bool, exc_type: Type[BaseException], err_msg: str | None = None) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Asserts a given condition is `True`.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
condition (bool): Condition which must evaluate to `True`.
|
|
14
|
+
exc_type (Type[BaseException]): Type of the exception to be raised if condition is `False`.
|
|
15
|
+
err_msg (str | None, optional): Custom error message used if assertion fails.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
exc_type: If condition is `False`.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
bool: If function returns, it will always return `True`.
|
|
22
|
+
"""
|
|
23
|
+
assert builtins.isinstance(condition, bool)
|
|
24
|
+
assert builtins.issubclass(exc_type, BaseException)
|
|
25
|
+
assert builtins.isinstance(err_msg, (str, type(None)))
|
|
26
|
+
|
|
27
|
+
if not condition:
|
|
28
|
+
if err_msg is None:
|
|
29
|
+
err_msg = "Condition is False"
|
|
30
|
+
|
|
31
|
+
raise exc_type(err_msg)
|
|
32
|
+
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
def isinstance(obj: Any, class_or_tuple: Type | tuple[Type, ...], err_msg: str | None = None) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Standardized assert for isinstance checks.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
obj (Any):
|
|
41
|
+
Inspected object.
|
|
42
|
+
|
|
43
|
+
class_or_tuple (type | tuple[type, ...]):
|
|
44
|
+
A type or a list of types.
|
|
45
|
+
|
|
46
|
+
err_msg (str | None, optional):
|
|
47
|
+
Custom error message used if assertion fails.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeError: If `obj` is not an instance of expected types.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
bool: If function returns, it will always return `True`.
|
|
54
|
+
"""
|
|
55
|
+
return check(
|
|
56
|
+
builtins.isinstance(obj, class_or_tuple),
|
|
57
|
+
TypeError,
|
|
58
|
+
f"Invalid type: {'None' if type(obj) == type(None) else type(obj).__name__}" if err_msg is None else err_msg
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def issubclass(cls: Type, class_or_tuple: Type | tuple[Type, ...], err_msg: str | None = None) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Standardized assert for issubclass checks.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
cls (Type):
|
|
67
|
+
Inspected given type.
|
|
68
|
+
|
|
69
|
+
class_or_tuple (type | tuple[type, ...]):
|
|
70
|
+
Accepted parent classes.
|
|
71
|
+
|
|
72
|
+
err_msg (str | None, optional):
|
|
73
|
+
Custom error message used if assertion fails.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If `cls` is not a subclass of any of provided types.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: If function returns, it will always return `True`.
|
|
80
|
+
"""
|
|
81
|
+
return check(
|
|
82
|
+
builtins.issubclass(cls, class_or_tuple),
|
|
83
|
+
TypeError,
|
|
84
|
+
f"{cls.__name__} is not a valid type" if err_msg is None else err_msg
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def check_value(condition: bool, err_msg: str | None = None) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Standardized way of check if a value meets a certain criteria.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
condition (bool):
|
|
93
|
+
Tested condition.
|
|
94
|
+
|
|
95
|
+
var_name (str, optional):
|
|
96
|
+
Variable name (used upon error).
|
|
97
|
+
|
|
98
|
+
err_msg (str | None, optional):
|
|
99
|
+
Custom error message used if assertion fails.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If `condition` is `False`.
|
|
103
|
+
"""
|
|
104
|
+
return check(condition, ValueError, err_msg)
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# Copyright (c) 2026 Leandro José Britto de Oliveira
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from . import assertions
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from io import IOBase, BytesIO, TextIOWrapper
|
|
8
|
+
from typing import Any, IO
|
|
9
|
+
|
|
10
|
+
import locale
|
|
11
|
+
import os
|
|
12
|
+
import selectors
|
|
13
|
+
import shlex
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Result:
|
|
20
|
+
"""
|
|
21
|
+
Process execution result.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
exit_code (int): Process exit code.
|
|
25
|
+
|
|
26
|
+
stdout (str | bytes | None):
|
|
27
|
+
Data captured from process stdout. It process was open in text mode,
|
|
28
|
+
it is a string, otherwise it is composed by raw bytes. If capturing
|
|
29
|
+
was not set during process creation, it is `None`.
|
|
30
|
+
|
|
31
|
+
stderr (str | bytes | None):
|
|
32
|
+
Data captured from process stderr. It process was open in text mode,
|
|
33
|
+
it is a string, otherwise it is composed by raw bytes. If capturing
|
|
34
|
+
was not set during process creation, it is `None`.
|
|
35
|
+
"""
|
|
36
|
+
exit_code: int
|
|
37
|
+
stdout: str | bytes | None
|
|
38
|
+
stderr: str | bytes | None
|
|
39
|
+
|
|
40
|
+
class Error(Exception):
|
|
41
|
+
"""
|
|
42
|
+
Process execution error.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, result: Result) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Process execution error.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
result (Result): Execution result.
|
|
51
|
+
"""
|
|
52
|
+
assertions.isinstance(result, Result)
|
|
53
|
+
assertions.check_value(result.exit_code != 0)
|
|
54
|
+
|
|
55
|
+
super().__init__(f"Process exited with code {result.exit_code}")
|
|
56
|
+
self.__result = result
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def result(self) -> Result:
|
|
60
|
+
"""
|
|
61
|
+
Execution result.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Result: Execution result.
|
|
65
|
+
"""
|
|
66
|
+
return self.__result
|
|
67
|
+
|
|
68
|
+
class Listener(ABC):
|
|
69
|
+
"""
|
|
70
|
+
Listener for a process.
|
|
71
|
+
"""
|
|
72
|
+
@classmethod
|
|
73
|
+
def decode(cls, data: bytes) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Decodes raw bytes into a string using system defaults.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data (bytes): raw bytes.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: Decoded string.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if not hasattr(Listener.decode, "encoding"):
|
|
85
|
+
encoding = locale.getpreferredencoding(False)
|
|
86
|
+
|
|
87
|
+
return data.decode(encoding, errors="replace")
|
|
88
|
+
|
|
89
|
+
def __init__(self, interactive: bool = True):
|
|
90
|
+
"""
|
|
91
|
+
Listener for a process.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
interactive (bool, optional):
|
|
95
|
+
Defines if this listener interacts with the process.
|
|
96
|
+
Defaults to True.
|
|
97
|
+
"""
|
|
98
|
+
assertions.isinstance(interactive, bool)
|
|
99
|
+
self.__interactive = interactive
|
|
100
|
+
super().__init__()
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def interactive(self) -> bool:
|
|
104
|
+
"""_summary_
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
bool: _description_
|
|
108
|
+
"""
|
|
109
|
+
return self.__interactive
|
|
110
|
+
|
|
111
|
+
def on_open(self, stdin: IOBase | None):
|
|
112
|
+
"""
|
|
113
|
+
Called upon process start.
|
|
114
|
+
|
|
115
|
+
Default implementation does nothing.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
stdin (IOBase | None):
|
|
119
|
+
Process stdin. Use it to send data to the process. If listener
|
|
120
|
+
is interactive, this will be a valide pipe to communicate
|
|
121
|
+
with the process. Otherwise, it is `None`.
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def on_read(
|
|
127
|
+
self,
|
|
128
|
+
stdin: IOBase | None,
|
|
129
|
+
stdout_data: str | bytes | None,
|
|
130
|
+
stderr_data: str | bytes | None
|
|
131
|
+
):
|
|
132
|
+
"""Called upon data is read from the process.
|
|
133
|
+
|
|
134
|
+
This is an abstract method.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
stdin (IOBase):
|
|
138
|
+
Process stdin. Use it to send data to the process. If listener
|
|
139
|
+
is interactive, this will be a valide pipe to communicate
|
|
140
|
+
with the process. Otherwise, it is `None`.
|
|
141
|
+
|
|
142
|
+
stdout_data (str | bytes | None):
|
|
143
|
+
Read data. `None` if no data was read from stdout.
|
|
144
|
+
If process was open in text mode, this argument will be a
|
|
145
|
+
string. Otherwise it will be raw data.
|
|
146
|
+
|
|
147
|
+
stderr_data (str | bytes | None):
|
|
148
|
+
Read data. `None` if no data was read from stderr.
|
|
149
|
+
If process was open in text mode, this argument will be a
|
|
150
|
+
string. Otherwise it will be raw data.
|
|
151
|
+
"""
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def on_close(self, exit_code: int):
|
|
155
|
+
"""
|
|
156
|
+
Called upon process finish.
|
|
157
|
+
|
|
158
|
+
Default implementation does nothing.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
exit_code (int):
|
|
162
|
+
Process exit code.
|
|
163
|
+
"""
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def open(
|
|
167
|
+
cmd: str|list[str],
|
|
168
|
+
text_mode: bool = True,
|
|
169
|
+
env: dict[str, str] | None = None,
|
|
170
|
+
stdin: IO[Any] | None = None,
|
|
171
|
+
stdout: IO[Any] | None = None,
|
|
172
|
+
stderr: IO[Any] | None = None,
|
|
173
|
+
capture_output: bool = False,
|
|
174
|
+
listener: Listener | None = None,
|
|
175
|
+
check_exit: bool = False,
|
|
176
|
+
timeout: float | None = None
|
|
177
|
+
) -> Result:
|
|
178
|
+
"""
|
|
179
|
+
Executes a process and wait for its completion.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
cmd (str | list[str]):
|
|
183
|
+
Command to be executed.
|
|
184
|
+
|
|
185
|
+
text_mode (bool, Optional):
|
|
186
|
+
Interpret output as text. Defaults to `True`.
|
|
187
|
+
|
|
188
|
+
env (dict[str, str] | None, Optional):
|
|
189
|
+
Custom environment for the process. Passsing `None` means inheriting
|
|
190
|
+
parent environment. Defaults to `None`.
|
|
191
|
+
|
|
192
|
+
stdin (IOBase, Optional):
|
|
193
|
+
IOBase instance to attach to process stdin. Defaults to `None`.
|
|
194
|
+
|
|
195
|
+
stdout (IOBase | None, Optional):
|
|
196
|
+
IOBase instance to attach to process stdout. Defaults to `None`.
|
|
197
|
+
|
|
198
|
+
stderr (IOBase | None, Optional):
|
|
199
|
+
IOBase instance to attach to process stderr. Defaults to `None`.
|
|
200
|
+
|
|
201
|
+
capture_output (bool, Optional):
|
|
202
|
+
Defines if output shall be captured to be read after process
|
|
203
|
+
finishes.
|
|
204
|
+
|
|
205
|
+
listener (Listener | None, Optional):
|
|
206
|
+
Listener instance used to monitor/interact with process. Defaults
|
|
207
|
+
to `None`. NOTE: It is not possible to pass an interactive
|
|
208
|
+
listener along with `stdin`.
|
|
209
|
+
|
|
210
|
+
check_exit (bool, Optional):
|
|
211
|
+
If `True`, raises an _Error_ if exit code is not zero.
|
|
212
|
+
|
|
213
|
+
timeout (int | None):
|
|
214
|
+
Timeout (in seconds) waiting for process to terminate. Passing
|
|
215
|
+
`None` means waiting until ti finishes. Defaults to `None`
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
Error:
|
|
219
|
+
If process exited with a non-zero code and `check_exit` is `True`.
|
|
220
|
+
|
|
221
|
+
TimeoutError:
|
|
222
|
+
If a timeout was reached an process did not terminate (it will
|
|
223
|
+
be killed).
|
|
224
|
+
|
|
225
|
+
ValueError:
|
|
226
|
+
If `stdin` was given along with an interactive `listener`.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Result: Result of process execution (note that function does not return
|
|
230
|
+
if process exited with a non-zero value and `check_exit` was `True`. For
|
|
231
|
+
this case use raised exception to get details about execution result).
|
|
232
|
+
"""
|
|
233
|
+
assertions.check_value(assertions.isinstance(cmd, (str, list)) and bool(cmd))
|
|
234
|
+
assertions.isinstance(text_mode, bool)
|
|
235
|
+
assertions.isinstance(env, (dict, type(None)))
|
|
236
|
+
assertions.isinstance(stdin, (type(None), IOBase))
|
|
237
|
+
assertions.isinstance(stdout, (type(None), IOBase))
|
|
238
|
+
assertions.isinstance(stderr, (type(None), IOBase))
|
|
239
|
+
assertions.isinstance(capture_output, bool)
|
|
240
|
+
assertions.isinstance(check_exit, bool)
|
|
241
|
+
assertions.isinstance(listener, (type(None), Listener))
|
|
242
|
+
assertions.isinstance(timeout, (float, int, type(None)))
|
|
243
|
+
|
|
244
|
+
if timeout is not None:
|
|
245
|
+
if isinstance(timeout, int):
|
|
246
|
+
timeout = float(timeout)
|
|
247
|
+
|
|
248
|
+
assert timeout > 0.0
|
|
249
|
+
|
|
250
|
+
if isinstance(cmd, str):
|
|
251
|
+
cmd = shlex.split(cmd)
|
|
252
|
+
|
|
253
|
+
if stdin is None:
|
|
254
|
+
proc_stdin = subprocess.DEVNULL # proc.stdin is None
|
|
255
|
+
else:
|
|
256
|
+
if listener is not None and listener.interactive:
|
|
257
|
+
raise ValueError("Cannot pass an interactive listener along with stdin")
|
|
258
|
+
|
|
259
|
+
if stdin is sys.stdin:
|
|
260
|
+
proc_stdin = None # proc.stdin is None (process inherits parent)
|
|
261
|
+
else:
|
|
262
|
+
proc_stdin = stdin # proc.stdin is NOT None and it will be provided one
|
|
263
|
+
|
|
264
|
+
if listener is not None and listener.interactive:
|
|
265
|
+
proc_stdin = subprocess.PIPE # Listener will communicate with the process
|
|
266
|
+
|
|
267
|
+
proc = subprocess.Popen(
|
|
268
|
+
cmd,
|
|
269
|
+
stdin=proc_stdin,
|
|
270
|
+
stdout=subprocess.PIPE,
|
|
271
|
+
stderr=subprocess.PIPE,
|
|
272
|
+
text=text_mode,
|
|
273
|
+
env=env
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
assert proc.stdout is not None
|
|
277
|
+
assert proc.stderr is not None
|
|
278
|
+
|
|
279
|
+
sel = selectors.DefaultSelector()
|
|
280
|
+
|
|
281
|
+
sel.register(proc.stdout, selectors.EVENT_READ)
|
|
282
|
+
sel.register(proc.stderr, selectors.EVENT_READ)
|
|
283
|
+
|
|
284
|
+
stdout_buffer = None if not capture_output else BytesIO()
|
|
285
|
+
stderr_buffer = None if not capture_output else BytesIO()
|
|
286
|
+
|
|
287
|
+
def decode(data: bytes) -> bytes | str:
|
|
288
|
+
"""
|
|
289
|
+
Returns either given data or decoded version if `text_mode` is `True`.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
data (bytes): raw bytes.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
bytes | str: `data` if `text_mode` is `False`. Otherwise, returns
|
|
296
|
+
string resulting from decoding given data.
|
|
297
|
+
"""
|
|
298
|
+
if not text_mode:
|
|
299
|
+
return data
|
|
300
|
+
else:
|
|
301
|
+
return Listener.decode(data)
|
|
302
|
+
|
|
303
|
+
def read(stream: IOBase) -> bytes:
|
|
304
|
+
"""
|
|
305
|
+
Read all data (raw bytes) currently available from an IO
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
stream (IOBase): IO object to read from.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
bytes: Read data (raw bytes).
|
|
312
|
+
"""
|
|
313
|
+
chunks = []
|
|
314
|
+
while True:
|
|
315
|
+
try:
|
|
316
|
+
if isinstance(stream, TextIOWrapper):
|
|
317
|
+
chunk = stream.buffer.read()
|
|
318
|
+
else:
|
|
319
|
+
chunk = stream.read()
|
|
320
|
+
|
|
321
|
+
if not chunk:
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
chunks.append(chunk)
|
|
325
|
+
except BlockingIOError:
|
|
326
|
+
# No more data available right now
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
return b"".join(chunks)
|
|
330
|
+
|
|
331
|
+
#if isinstance(stream, TextIOWrapper):
|
|
332
|
+
# return stream.buffer.read1(chunk_size)
|
|
333
|
+
#else:
|
|
334
|
+
# return stream.read1(chunk_size)
|
|
335
|
+
|
|
336
|
+
def write(data: bytes, stream: IO[Any]):
|
|
337
|
+
"""
|
|
338
|
+
Writes raw bytes into an IO object.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
data (bytes): Raw data to be written.
|
|
342
|
+
stream (IO[Any]): IO object.
|
|
343
|
+
"""
|
|
344
|
+
if isinstance(stream, TextIOWrapper):
|
|
345
|
+
stream.buffer.write(data)
|
|
346
|
+
stream.buffer.flush()
|
|
347
|
+
else:
|
|
348
|
+
stream.write(data)
|
|
349
|
+
stream.flush()
|
|
350
|
+
|
|
351
|
+
# Make pipes non-block so it is possible to read all data available
|
|
352
|
+
# upon selector events (Works on all platforms, including Windows)
|
|
353
|
+
os.set_blocking(proc.stdout.fileno(), False)
|
|
354
|
+
os.set_blocking(proc.stderr.fileno(), False)
|
|
355
|
+
|
|
356
|
+
listener_stdin = None
|
|
357
|
+
if stdin is None and listener is not None:
|
|
358
|
+
# Pass the pipe to the listener so it can communicate with the process.
|
|
359
|
+
listener_stdin = proc.stdin
|
|
360
|
+
|
|
361
|
+
if proc.poll() is None and listener is not None:
|
|
362
|
+
listener.on_open(listener_stdin)
|
|
363
|
+
|
|
364
|
+
mark = time.perf_counter()
|
|
365
|
+
remaining: float | None = timeout
|
|
366
|
+
|
|
367
|
+
while remaining is None or remaining >= 0:
|
|
368
|
+
events = sel.select(remaining)
|
|
369
|
+
for key, _ in events:
|
|
370
|
+
while True:
|
|
371
|
+
data = read(key.fileobj)
|
|
372
|
+
decoded_data = decode(data)
|
|
373
|
+
|
|
374
|
+
if not data:
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
if key.fileobj is proc.stdout:
|
|
378
|
+
# process stdout
|
|
379
|
+
if capture_output:
|
|
380
|
+
stdout_buffer.write(data)
|
|
381
|
+
|
|
382
|
+
if stdout is not None:
|
|
383
|
+
write(data, stdout)
|
|
384
|
+
|
|
385
|
+
if listener is not None:
|
|
386
|
+
listener.on_read(listener_stdin, decoded_data, None)
|
|
387
|
+
|
|
388
|
+
elif key.fileobj is proc.stderr:
|
|
389
|
+
# process stderr
|
|
390
|
+
if capture_output:
|
|
391
|
+
stderr_buffer.write(data)
|
|
392
|
+
|
|
393
|
+
if stderr:
|
|
394
|
+
write(data, stderr)
|
|
395
|
+
|
|
396
|
+
if listener is not None:
|
|
397
|
+
listener.on_read(listener_stdin, None, decoded_data)
|
|
398
|
+
|
|
399
|
+
# If process exited, break
|
|
400
|
+
if proc.poll() is not None:
|
|
401
|
+
exit_code = proc.wait()
|
|
402
|
+
|
|
403
|
+
if listener is not None:
|
|
404
|
+
listener.on_close(exit_code)
|
|
405
|
+
|
|
406
|
+
stdout_data = None if not capture_output else decode(stdout_buffer.getvalue())
|
|
407
|
+
stderr_data = None if not capture_output else decode(stderr_buffer.getvalue())
|
|
408
|
+
|
|
409
|
+
result = Result(exit_code, stdout_data, stderr_data)
|
|
410
|
+
|
|
411
|
+
if exit_code != 0 and check_exit:
|
|
412
|
+
raise Error(result)
|
|
413
|
+
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
elapsed = time.perf_counter() - mark
|
|
417
|
+
remaining = None if remaining is None else remaining - elapsed
|
|
418
|
+
|
|
419
|
+
proc.kill()
|
|
420
|
+
raise TimeoutError()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agapsys-utils
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Common utilities
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: build; extra == "dev"
|
|
11
|
+
Requires-Dist: twine; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# agapsys-utils
|
|
17
|
+
|
|
18
|
+
This project provides general purpose utiltities that individually thet do not fit in a dedicated library.
|
|
19
|
+
|
|
20
|
+
## License
|
|
21
|
+
|
|
22
|
+
m-conf is distributed under MIT License. Please see the [LICENSE](LICENSE) file for details on copying and distribution.
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
```py
|
|
27
|
+
import agapsys.utils as utils
|
|
28
|
+
|
|
29
|
+
# That's it... use the available modules
|
|
30
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/agapsys/utils/__init__.py
|
|
5
|
+
src/agapsys/utils/assertions.py
|
|
6
|
+
src/agapsys/utils/proc.py
|
|
7
|
+
src/agapsys_utils.egg-info/PKG-INFO
|
|
8
|
+
src/agapsys_utils.egg-info/SOURCES.txt
|
|
9
|
+
src/agapsys_utils.egg-info/dependency_links.txt
|
|
10
|
+
src/agapsys_utils.egg-info/requires.txt
|
|
11
|
+
src/agapsys_utils.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agapsys
|