pyosal 0.1.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.
- pyosal-0.1.0/LICENSE.txt +23 -0
- pyosal-0.1.0/MANIFEST.in +2 -0
- pyosal-0.1.0/PKG-INFO +28 -0
- pyosal-0.1.0/README.md +6 -0
- pyosal-0.1.0/pyproject.toml +53 -0
- pyosal-0.1.0/setup.cfg +43 -0
- pyosal-0.1.0/src/pyosal/__init__.py +3 -0
- pyosal-0.1.0/src/pyosal/constants.py +10 -0
- pyosal-0.1.0/src/pyosal/constants_version.py +9 -0
- pyosal-0.1.0/src/pyosal/pyosal.py +565 -0
- pyosal-0.1.0/src/pyosal.egg-info/PKG-INFO +28 -0
- pyosal-0.1.0/src/pyosal.egg-info/SOURCES.txt +13 -0
- pyosal-0.1.0/src/pyosal.egg-info/dependency_links.txt +1 -0
- pyosal-0.1.0/src/pyosal.egg-info/top_level.txt +1 -0
pyosal-0.1.0/LICENSE.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) since 2026: pyosal Python Module
|
|
4
|
+
description: python Operating System Abstract Layer
|
|
5
|
+
author: J. Arrizza email: cppgent0@gmail.com
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
pyosal-0.1.0/MANIFEST.in
ADDED
pyosal-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyosal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: python Operating System Abstract Layer
|
|
5
|
+
Author-email: "J. Arrizza" <cppgent0@gmail.com>
|
|
6
|
+
Maintainer-email: "J. Arrizza" <cppgent0@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Website, https://arrizza.com/pyosal
|
|
9
|
+
Project-URL: Source, https://bitbucket.org/arrizza-public/pyosal/src/master
|
|
10
|
+
Project-URL: Download, https://bitbucket.org/arrizza-public/pyosal/get/master.zip
|
|
11
|
+
Keywords: SHOULD-NOT-MATCH,SHOULD-NOT-MATCH,SHOULD-NOT-MATCH
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.txt
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
* website: [Website](https://arrizza.com/TODO.html)
|
|
24
|
+
* installation: [Common Setup](https://arrizza.com/setup-common.html)
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
***Under Construction***
|
pyosal-0.1.0/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
description = 'python Operating System Abstract Layer'
|
|
3
|
+
keywords = ['SHOULD-NOT-MATCH', 'SHOULD-NOT-MATCH', 'SHOULD-NOT-MATCH']
|
|
4
|
+
|
|
5
|
+
classifiers = [
|
|
6
|
+
# "Private :: Do Not Upload", # use to prevent upload
|
|
7
|
+
# Choose either '3 - Alpha', '4 - Beta' or '5 - Production/Stable'
|
|
8
|
+
'Development Status :: 3 - Alpha',
|
|
9
|
+
'Environment :: Console',
|
|
10
|
+
'Topic :: Utilities',
|
|
11
|
+
'Intended Audience :: Developers',
|
|
12
|
+
'Programming Language :: Python :: 3.10',
|
|
13
|
+
'Programming Language :: Python :: 3.12',
|
|
14
|
+
]
|
|
15
|
+
# additional packages to install
|
|
16
|
+
dependencies = [
|
|
17
|
+
# 'falcon-logger',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# common values
|
|
21
|
+
name = 'pyosal'
|
|
22
|
+
version = '0.1.0'
|
|
23
|
+
requires-python = '>=3.9'
|
|
24
|
+
authors = [{ name = 'J. Arrizza', email = 'cppgent0@gmail.com' }]
|
|
25
|
+
maintainers = [
|
|
26
|
+
{ name = 'J. Arrizza', email = 'cppgent0@gmail.com' },
|
|
27
|
+
]
|
|
28
|
+
readme = { file = 'README.md', content-type = 'text/markdown' }
|
|
29
|
+
license = 'MIT'
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
# None
|
|
33
|
+
[project.scripts]
|
|
34
|
+
# None e.g. spam-cli = "spam:main_cli"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
'Website' = 'https://arrizza.com/pyosal'
|
|
38
|
+
'Source' = 'https://bitbucket.org/arrizza-public/pyosal/src/master'
|
|
39
|
+
'Download' = 'https://bitbucket.org/arrizza-public/pyosal/get/master.zip'
|
|
40
|
+
# 'Bug Reports' = 'TODO https://github.com/pypa/sampleproject/issues'
|
|
41
|
+
# 'Funding' = 'TODO https://donate.pypi.org'
|
|
42
|
+
# 'Say Thanks!' = 'TODO http://saythanks.io/to/example'
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ['src']
|
|
46
|
+
exclude = ['.gitignore']
|
|
47
|
+
|
|
48
|
+
[tool.setuptools]
|
|
49
|
+
include-package-data = true
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ['setuptools >=61.0']
|
|
53
|
+
build-backend = 'setuptools.build_meta'
|
pyosal-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
description_file = README.md
|
|
3
|
+
|
|
4
|
+
[pylint.MESSAGES CONTROL]
|
|
5
|
+
max-line-length = 140
|
|
6
|
+
disable = C0114,C0115,C0116,C0415,R0801,R0903,R0912,R0913,R0917,W0511
|
|
7
|
+
|
|
8
|
+
[pycodestyle]
|
|
9
|
+
max-line-length = 140
|
|
10
|
+
ignore = E266,E402,E731,E203
|
|
11
|
+
statistics = True
|
|
12
|
+
exclude = venv/*, out/*, pf_*, setup.py, debug/*, release/*
|
|
13
|
+
|
|
14
|
+
[coverage:run]
|
|
15
|
+
omit =
|
|
16
|
+
./conftest.py
|
|
17
|
+
./out/*
|
|
18
|
+
./ut/*
|
|
19
|
+
./venv/*
|
|
20
|
+
./ver/*
|
|
21
|
+
build_info.py
|
|
22
|
+
branch = true
|
|
23
|
+
|
|
24
|
+
[coverage:report]
|
|
25
|
+
exclude_lines =
|
|
26
|
+
pragma: no cover
|
|
27
|
+
|
|
28
|
+
def __repr__
|
|
29
|
+
if self\.debug
|
|
30
|
+
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
if 0:
|
|
34
|
+
if __main__ == .__main__:
|
|
35
|
+
ignore_errors = True
|
|
36
|
+
|
|
37
|
+
[coverage:html]
|
|
38
|
+
directory = ./out/coverage
|
|
39
|
+
|
|
40
|
+
[egg_info]
|
|
41
|
+
tag_build =
|
|
42
|
+
tag_date = 0
|
|
43
|
+
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# -------------------
|
|
11
|
+
## Meta class for pyosal; ensures it is a singleton
|
|
12
|
+
class _PyOsalMeta(type):
|
|
13
|
+
## holds instances of this class (should be 1)
|
|
14
|
+
_instances = {}
|
|
15
|
+
## ensure creation is thread safe
|
|
16
|
+
_lock = threading.Lock()
|
|
17
|
+
|
|
18
|
+
# -------------------
|
|
19
|
+
## handle getting the instance of the class.
|
|
20
|
+
# @param args the arguments
|
|
21
|
+
# @param kwargs the kw arguments
|
|
22
|
+
def __call__(cls, *args, **kwargs):
|
|
23
|
+
# Double-checked locking for performance and thread safety
|
|
24
|
+
if cls not in cls._instances: # pragma: no cover
|
|
25
|
+
with cls._lock:
|
|
26
|
+
if cls not in cls._instances: # pragma: no cover
|
|
27
|
+
# super().__call__ handles object creation AND initialization
|
|
28
|
+
instance = super().__call__(*args, **kwargs)
|
|
29
|
+
cls._instances[cls] = instance
|
|
30
|
+
return cls._instances[cls]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# -------------------
|
|
34
|
+
## holds OS specific info; there are four recognized OS:
|
|
35
|
+
# OS os_name
|
|
36
|
+
# ------- ----------
|
|
37
|
+
# Ubuntu : ubuntu
|
|
38
|
+
# Mac : macos
|
|
39
|
+
# Windows : win (msys2)
|
|
40
|
+
# Windows : psi (powershell)
|
|
41
|
+
# RPI : rpi
|
|
42
|
+
class PyOsal(metaclass=_PyOsalMeta):
|
|
43
|
+
# -------------------
|
|
44
|
+
## initialize
|
|
45
|
+
def __init__(self):
|
|
46
|
+
## holds the OS name
|
|
47
|
+
self._os_name: str = None
|
|
48
|
+
## holds the OS version info
|
|
49
|
+
self._os_version: str
|
|
50
|
+
## holds the python version
|
|
51
|
+
self._python_version: str = None
|
|
52
|
+
## root directory of projects directory
|
|
53
|
+
# note returns windows format C:/xx/xx
|
|
54
|
+
self._root_dir: str = None
|
|
55
|
+
## root directory of projects directory in msys2 format /c/xx/xx
|
|
56
|
+
self._root_dir_msys2: str = None
|
|
57
|
+
## current userid
|
|
58
|
+
self._userid: str = None
|
|
59
|
+
## hostname of the pc
|
|
60
|
+
self._hostname: str = None
|
|
61
|
+
## list of valid OS
|
|
62
|
+
self._os_valid = ['ubuntu', 'macos', 'msys2', 'psi', 'rpi']
|
|
63
|
+
|
|
64
|
+
# === UT related
|
|
65
|
+
|
|
66
|
+
## used by UTs only
|
|
67
|
+
self._ut_mode = False
|
|
68
|
+
## used by UTs only
|
|
69
|
+
self.ut_curr_dir = None
|
|
70
|
+
|
|
71
|
+
# === initialize
|
|
72
|
+
self._get_os_name()
|
|
73
|
+
self._set_all_data()
|
|
74
|
+
|
|
75
|
+
# -------------------
|
|
76
|
+
## get OS name/tag
|
|
77
|
+
# @return os_name
|
|
78
|
+
@property
|
|
79
|
+
def os_name(self):
|
|
80
|
+
return self._os_name
|
|
81
|
+
|
|
82
|
+
# -------------------
|
|
83
|
+
## get OS version info
|
|
84
|
+
# @return os_version
|
|
85
|
+
@property
|
|
86
|
+
def os_version(self):
|
|
87
|
+
return self._os_version
|
|
88
|
+
|
|
89
|
+
# -------------------
|
|
90
|
+
## get python version
|
|
91
|
+
# @return python_version
|
|
92
|
+
@property
|
|
93
|
+
def python_version(self):
|
|
94
|
+
return self._python_version
|
|
95
|
+
|
|
96
|
+
# -------------------
|
|
97
|
+
## get path for root directory
|
|
98
|
+
# @return root_dir
|
|
99
|
+
@property
|
|
100
|
+
def root_dir(self):
|
|
101
|
+
return self._root_dir
|
|
102
|
+
|
|
103
|
+
# -------------------
|
|
104
|
+
## get root directory for msys2 shells
|
|
105
|
+
# @return root_dir_msys2
|
|
106
|
+
@property
|
|
107
|
+
def root_dir_msys2(self):
|
|
108
|
+
return self._root_dir_msys2
|
|
109
|
+
|
|
110
|
+
# -------------------
|
|
111
|
+
## get current userid
|
|
112
|
+
# @return userid
|
|
113
|
+
@property
|
|
114
|
+
def userid(self):
|
|
115
|
+
return self._userid
|
|
116
|
+
|
|
117
|
+
# -------------------
|
|
118
|
+
## get current PC's hostname
|
|
119
|
+
# @return hostname
|
|
120
|
+
@property
|
|
121
|
+
def hostname(self):
|
|
122
|
+
return self._hostname
|
|
123
|
+
|
|
124
|
+
# -------------------
|
|
125
|
+
## get list of recognized valid OS names
|
|
126
|
+
# @return os_valid
|
|
127
|
+
@property
|
|
128
|
+
def os_valid(self):
|
|
129
|
+
return self._os_valid
|
|
130
|
+
|
|
131
|
+
# -------------------
|
|
132
|
+
## selects the current platform and sets impl to it
|
|
133
|
+
#
|
|
134
|
+
# @return None
|
|
135
|
+
def _set_all_data(self):
|
|
136
|
+
if self._os_name == 'rpi':
|
|
137
|
+
# TODO confirm this is correct
|
|
138
|
+
self._os_version = f'RPI {platform.system()} {platform.release()}'
|
|
139
|
+
self._root_dir = '~'
|
|
140
|
+
self._root_dir_msys2 = self._root_dir
|
|
141
|
+
elif self._os_name == 'msys2':
|
|
142
|
+
self._os_version = f'win32 {platform.system()} {platform.release()}'
|
|
143
|
+
self._root_dir = os.path.expanduser('~')
|
|
144
|
+
self._root_dir = self._root_dir.replace('\\', '/')
|
|
145
|
+
self._root_dir_msys2 = '/' + self._root_dir[0].lower() + self._root_dir[2:]
|
|
146
|
+
elif self._os_name == 'macos':
|
|
147
|
+
if self._ut_mode: # pragma: no cover
|
|
148
|
+
self._os_version = 'macOS 14.5'
|
|
149
|
+
else:
|
|
150
|
+
self._os_version = f'macOS {platform.mac_ver()[0]}' # pragma: no cover
|
|
151
|
+
self._root_dir = '~'
|
|
152
|
+
self._root_dir_msys2 = self._root_dir
|
|
153
|
+
elif self._os_name == 'ubuntu':
|
|
154
|
+
if self._ut_mode:
|
|
155
|
+
self._os_version = 'Ubuntu 24.04 noble'
|
|
156
|
+
else:
|
|
157
|
+
self._os_version = self._ubuntu_os_version()
|
|
158
|
+
self._root_dir = '~'
|
|
159
|
+
self._root_dir_msys2 = self._root_dir
|
|
160
|
+
else:
|
|
161
|
+
self.abort(f'PyOsal: unrecognized OS: "{sys.platform}"')
|
|
162
|
+
|
|
163
|
+
## holds python version e.g. "Python 3.10"
|
|
164
|
+
self._python_version = f'Python {sys.version_info.major}.{sys.version_info.minor}'
|
|
165
|
+
|
|
166
|
+
self._hostname = platform.uname().node
|
|
167
|
+
self._hostname = self._hostname.lower().replace('.local', '')
|
|
168
|
+
|
|
169
|
+
self._userid = getpass.getuser()
|
|
170
|
+
|
|
171
|
+
# -------------------
|
|
172
|
+
## get the current OS tag
|
|
173
|
+
# @return None
|
|
174
|
+
def _get_os_name(self): # pragma: no cover
|
|
175
|
+
if os.path.isfile('/sys/firmware/devicetree/base/model'):
|
|
176
|
+
self._os_name = 'rpi'
|
|
177
|
+
elif sys.platform == 'win32':
|
|
178
|
+
# TODO check if it's a cygwin?
|
|
179
|
+
if 'MSYSTEM' in os.environ:
|
|
180
|
+
self._os_name = 'msys2'
|
|
181
|
+
elif 'PSModulePath' in os.environ or 'POWERSHELL_DISTRIBUTION_CHANNEL' in os.environ:
|
|
182
|
+
self._os_name = 'psi'
|
|
183
|
+
else:
|
|
184
|
+
self._os_name = 'win-unknown'
|
|
185
|
+
elif sys.platform == 'darwin':
|
|
186
|
+
self._os_name = 'macos'
|
|
187
|
+
elif sys.platform == 'linux':
|
|
188
|
+
self._os_name = 'ubuntu'
|
|
189
|
+
else:
|
|
190
|
+
self._os_name = 'unknown'
|
|
191
|
+
|
|
192
|
+
# -------------------
|
|
193
|
+
## holds OS information for Ubuntu
|
|
194
|
+
#
|
|
195
|
+
# @return string indicating OS info
|
|
196
|
+
def _ubuntu_os_version(self):
|
|
197
|
+
proc = subprocess.Popen( # pylint: disable=consider-using-with
|
|
198
|
+
['lsb_release', '-a'],
|
|
199
|
+
stdout=subprocess.PIPE,
|
|
200
|
+
stderr=subprocess.STDOUT,
|
|
201
|
+
)
|
|
202
|
+
(out, _) = proc.communicate()
|
|
203
|
+
out = out.decode('utf-8')
|
|
204
|
+
|
|
205
|
+
version = 'notset'
|
|
206
|
+
codename = 'notset'
|
|
207
|
+
for line in out.split('\n'):
|
|
208
|
+
# sys.stdout.write(f'line: "{line}"')
|
|
209
|
+
args = line.split('\t')
|
|
210
|
+
if args[0] == 'Release:':
|
|
211
|
+
version = args[1]
|
|
212
|
+
elif args[0] == 'Codename:':
|
|
213
|
+
codename = args[1]
|
|
214
|
+
return f'Ubuntu {version} {codename}'
|
|
215
|
+
|
|
216
|
+
# === cmd invocation from here on
|
|
217
|
+
|
|
218
|
+
## exception keywords
|
|
219
|
+
_excp_keywords = [
|
|
220
|
+
'Traceback (most recent call last):',
|
|
221
|
+
'Error:',
|
|
222
|
+
'Permission denied',
|
|
223
|
+
'command not found',
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# -------------------
|
|
227
|
+
## run a command in a subprocess
|
|
228
|
+
#
|
|
229
|
+
# @param cmd the command to run
|
|
230
|
+
# @param working_dir the working directory to run the command in; default is '.'
|
|
231
|
+
# @param use_raw_log no prefix on line
|
|
232
|
+
# @param log_file the log_file to write the lines to
|
|
233
|
+
# @param print_cb callback function used to print output; default is None
|
|
234
|
+
# @param env the environment variables to use
|
|
235
|
+
# @param quiet if True, do not write to stdout
|
|
236
|
+
# @return rc - return code, lines - the output as a list of lines
|
|
237
|
+
def run_cmd( # pylint: disable=too-many-branches,too-many-statements
|
|
238
|
+
self,
|
|
239
|
+
cmd,
|
|
240
|
+
working_dir='.',
|
|
241
|
+
use_raw_log=False,
|
|
242
|
+
log_file=None,
|
|
243
|
+
print_cb=None,
|
|
244
|
+
env=None,
|
|
245
|
+
quiet=False,
|
|
246
|
+
): # pylint: disable=too-many-locals
|
|
247
|
+
if not isinstance(cmd, list):
|
|
248
|
+
self.abort('PyOsal: cmd must be a list')
|
|
249
|
+
|
|
250
|
+
## os_name: see above
|
|
251
|
+
if self._os_name == 'msys2':
|
|
252
|
+
# need to invokve msys2 bash
|
|
253
|
+
pfx = ['c:/msys64/usr/bin/bash', '-c']
|
|
254
|
+
# the current cmd is a single string at the end
|
|
255
|
+
pfx.append(' '.join(cmd))
|
|
256
|
+
cmd = pfx
|
|
257
|
+
|
|
258
|
+
# msys2 can cause truncations and wrapping of text if the lines are long
|
|
259
|
+
if env is None:
|
|
260
|
+
env = os.environ.copy()
|
|
261
|
+
|
|
262
|
+
fp = None
|
|
263
|
+
if log_file is not None:
|
|
264
|
+
fp = open(log_file, 'w', encoding='utf-8', newline='\n') # pylint: disable=consider-using-with
|
|
265
|
+
|
|
266
|
+
# TODO set to 127 and then set specifically to rc == 0 below
|
|
267
|
+
rc = 0
|
|
268
|
+
lineno = 0
|
|
269
|
+
lines = []
|
|
270
|
+
|
|
271
|
+
# ensure all output is printed, so any stdin done by the cmd is visible in the right order
|
|
272
|
+
self.stdout_flush()
|
|
273
|
+
try:
|
|
274
|
+
proc = subprocess.Popen( # pylint: disable=consider-using-with
|
|
275
|
+
cmd,
|
|
276
|
+
cwd=working_dir,
|
|
277
|
+
bufsize=0,
|
|
278
|
+
universal_newlines=True,
|
|
279
|
+
errors='replace',
|
|
280
|
+
text=True,
|
|
281
|
+
stdin=subprocess.DEVNULL,
|
|
282
|
+
stdout=subprocess.PIPE,
|
|
283
|
+
stderr=subprocess.STDOUT,
|
|
284
|
+
env=env,
|
|
285
|
+
)
|
|
286
|
+
except Exception as excp: # pylint: disable=broad-exception-caught
|
|
287
|
+
err_type = excp.__class__.__name__
|
|
288
|
+
line = f'EXCP pyosal.run_cmd: {err_type}: {excp}'
|
|
289
|
+
self._save_line(line, lineno, lines, fp, print_cb, use_raw_log, quiet)
|
|
290
|
+
return rc, lines
|
|
291
|
+
|
|
292
|
+
buffer = ''
|
|
293
|
+
while True:
|
|
294
|
+
chunk = os.read(proc.stdout.fileno(), 256)
|
|
295
|
+
if not chunk:
|
|
296
|
+
break
|
|
297
|
+
chunk_str = chunk.decode('utf-8', errors='surrogateescape')
|
|
298
|
+
chunk_str = chunk_str.replace('\r', '')
|
|
299
|
+
|
|
300
|
+
# for char in chunk:
|
|
301
|
+
# sys.stdout.write(f"'{char!r}': 0x{ord(char):02x}")
|
|
302
|
+
|
|
303
|
+
buffer += chunk_str
|
|
304
|
+
if '\n' not in buffer:
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
buff_lines = buffer.split('\n')
|
|
308
|
+
buffer = buff_lines.pop()
|
|
309
|
+
|
|
310
|
+
for line in buff_lines:
|
|
311
|
+
# if not line.isprintable():
|
|
312
|
+
# sys.stdout.write('has hex: ', ascii(line), '\n')
|
|
313
|
+
|
|
314
|
+
# check for exceptions and other script failures
|
|
315
|
+
if any(keyword in line for keyword in self._excp_keywords):
|
|
316
|
+
excp_msg = '@@>>@@ Exception detected'
|
|
317
|
+
if excp_msg in line:
|
|
318
|
+
rc += 1
|
|
319
|
+
continue
|
|
320
|
+
sys.stdout.write(f' -- {excp_msg}: "{line}"\n')
|
|
321
|
+
rc += 1
|
|
322
|
+
|
|
323
|
+
if 'DEPRECATE' in line:
|
|
324
|
+
dep_msg = '@@>>@@ Deprecation detected'
|
|
325
|
+
if dep_msg in line:
|
|
326
|
+
rc += 1
|
|
327
|
+
continue
|
|
328
|
+
rc += 1
|
|
329
|
+
sys.stdout.write(f' -- {dep_msg} {rc}: "{line}"\n')
|
|
330
|
+
|
|
331
|
+
if self._skip_line(line):
|
|
332
|
+
line2 = line.encode('utf-8', errors='ignore')
|
|
333
|
+
sys.stdout.write(f'@@>>@@ line skipped: {line2}' + '\n')
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
# it has text, so add it to the buffer and do the callback if defined
|
|
337
|
+
lineno += 1
|
|
338
|
+
self._save_line(line, lineno, lines, fp, print_cb, use_raw_log, quiet)
|
|
339
|
+
|
|
340
|
+
# at this point: chunk was empty
|
|
341
|
+
if len(buffer) > 0:
|
|
342
|
+
buff_lines = buffer.split('\n')
|
|
343
|
+
for line in buff_lines:
|
|
344
|
+
lineno += 1
|
|
345
|
+
self._save_line(line, lineno, lines, fp, print_cb, use_raw_log, quiet)
|
|
346
|
+
|
|
347
|
+
if fp:
|
|
348
|
+
fp.flush()
|
|
349
|
+
fp.close()
|
|
350
|
+
|
|
351
|
+
rc += proc.wait()
|
|
352
|
+
|
|
353
|
+
if proc.stdout:
|
|
354
|
+
self.stdout_flush()
|
|
355
|
+
proc.stdout.close()
|
|
356
|
+
if proc.stderr:
|
|
357
|
+
self.stderr_flush()
|
|
358
|
+
proc.stderr.close()
|
|
359
|
+
|
|
360
|
+
return rc, lines
|
|
361
|
+
|
|
362
|
+
# -------------------
|
|
363
|
+
## skip lines with ansi escape sequeces, etc.
|
|
364
|
+
# @param line the line to check
|
|
365
|
+
# @return True if line should be skipped, False otherwise
|
|
366
|
+
def _skip_line(self, line: str) -> bool:
|
|
367
|
+
if line == '\x1b[?25l': # hide mouse cursor
|
|
368
|
+
return True
|
|
369
|
+
if line == '\x1b[?25h': # hide mouse cursor
|
|
370
|
+
return True
|
|
371
|
+
if re.search(r'^\x1b.*\d+%', line): # skip percent lines
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
if re.search(r'[\u2580-\u259F]', line):
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
# Catch carriage returns (common in curl/wget across Linux/macOS/MSYS2)
|
|
378
|
+
if '\r' in line:
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
# catch ANSI Escape Sequences (used for colors/moving cursors in bash/ps1)
|
|
382
|
+
# This strips \x1b[..., \033[..., etc.
|
|
383
|
+
# ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
384
|
+
# cleaned_line = ansi_escape.sub('', line)
|
|
385
|
+
|
|
386
|
+
# TODO fix; causes problems with coverage reports that has % in them
|
|
387
|
+
# e.g. functions: 100.0% (2 out of 2)
|
|
388
|
+
# Match progress percentages (e.g., 10%, 10.5%)
|
|
389
|
+
# if re.search(r'\d+(?:\.\d+)?%', cleaned_line):
|
|
390
|
+
# return True
|
|
391
|
+
|
|
392
|
+
# # Match interactive spinners (\ | / -)
|
|
393
|
+
# if re.search(r'(?:[\s\[\(\]{}]|^)([\\/|-]+)(?:[\s\]\)\}{}]|$)', cleaned_line):
|
|
394
|
+
# return True
|
|
395
|
+
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
# -------------------
|
|
399
|
+
## save the current line (if it's not empty)
|
|
400
|
+
# @param line the line to save
|
|
401
|
+
# @param lineno the line#
|
|
402
|
+
# @param lines the current list of lines
|
|
403
|
+
# @param fp file pointer to write line to; default is None
|
|
404
|
+
# @param print_cb callback function used to print output; default is None
|
|
405
|
+
# @param use_raw_log no prefix on line
|
|
406
|
+
# @param quiet if True, don't print to stdout, but still add to returned line
|
|
407
|
+
# @return None
|
|
408
|
+
def _save_line(self, line, lineno, lines, fp, print_cb, use_raw_log, quiet):
|
|
409
|
+
if not line:
|
|
410
|
+
# skip empty lines
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# always save the line
|
|
414
|
+
lines.append(line)
|
|
415
|
+
|
|
416
|
+
if quiet:
|
|
417
|
+
# caller doesn't want it to stdout, fp or print_cb
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
if use_raw_log:
|
|
421
|
+
line_to_save = line
|
|
422
|
+
else:
|
|
423
|
+
line_to_save = f' -- {line}'
|
|
424
|
+
line_to_save = line_to_save.replace('←', '\x1b')
|
|
425
|
+
sys.stdout.write(line_to_save + '\n')
|
|
426
|
+
self.stdout_flush()
|
|
427
|
+
|
|
428
|
+
if fp:
|
|
429
|
+
fp.write(line_to_save)
|
|
430
|
+
fp.write('\n')
|
|
431
|
+
|
|
432
|
+
if print_cb:
|
|
433
|
+
print_cb(lineno, line)
|
|
434
|
+
|
|
435
|
+
# --------------------
|
|
436
|
+
## flush for stdout
|
|
437
|
+
# @param force (optional) if True, do the buffer flush even if not msys2
|
|
438
|
+
def stdout_flush(self, force=False):
|
|
439
|
+
sys.stdout.flush()
|
|
440
|
+
if force or self._os_name == 'msys2':
|
|
441
|
+
if hasattr(sys.stdout, 'buffer'):
|
|
442
|
+
sys.stdout.buffer.flush()
|
|
443
|
+
|
|
444
|
+
# --------------------
|
|
445
|
+
## flush for stderr
|
|
446
|
+
# @param force (optional) if True, do the buffer flush even if not msys2
|
|
447
|
+
def stderr_flush(self, force=False):
|
|
448
|
+
sys.stderr.flush()
|
|
449
|
+
if force or self._os_name == 'msys2':
|
|
450
|
+
if hasattr(sys.stderr, 'buffer'):
|
|
451
|
+
sys.stderr.buffer.flush()
|
|
452
|
+
|
|
453
|
+
# --------------------
|
|
454
|
+
## convert a windows path to posix i.e. forward slashes
|
|
455
|
+
# @param path the path to fix
|
|
456
|
+
# @param force force the conversion no matter what the OS is
|
|
457
|
+
# @return the unfixed path
|
|
458
|
+
def xlat_to_win(self, path, force=False):
|
|
459
|
+
path = os.path.expanduser(path)
|
|
460
|
+
if force or self._os_name == 'msys2':
|
|
461
|
+
# assumes there is only one character and it is for a drive letter
|
|
462
|
+
# can only be done at the start of the path
|
|
463
|
+
m = re.search(r'^/(.)/(.*)', path)
|
|
464
|
+
if m:
|
|
465
|
+
drive = m.group(1).upper()
|
|
466
|
+
path = f'{drive}:/{m.group(2)}'
|
|
467
|
+
path = path.replace('/', '\\')
|
|
468
|
+
path = path.replace('//', '\\')
|
|
469
|
+
return path
|
|
470
|
+
|
|
471
|
+
# --------------------
|
|
472
|
+
## convert a windows path to posix i.e. forward slashes
|
|
473
|
+
# @param path the path to fix
|
|
474
|
+
# @param force force the conversion no matter what the OS is
|
|
475
|
+
# @return the unfixed path
|
|
476
|
+
def xlat_to_posix(self, path, force=False):
|
|
477
|
+
path = os.path.expanduser(path)
|
|
478
|
+
if force or self._os_name == 'msys2':
|
|
479
|
+
m = re.search(r'^(.):(.*)', path)
|
|
480
|
+
if m:
|
|
481
|
+
drive = m.group(1).lower()
|
|
482
|
+
path = f'/{drive}{m.group(2)}'
|
|
483
|
+
path = path.replace('\\', '/')
|
|
484
|
+
path = path.replace('//', '/')
|
|
485
|
+
return path
|
|
486
|
+
|
|
487
|
+
# -------------------
|
|
488
|
+
## convert a path to relative to home
|
|
489
|
+
# @param path the path to fix
|
|
490
|
+
# @return the relative path
|
|
491
|
+
def xlat_dir_to_relative(self, path):
|
|
492
|
+
path = self.xlat_to_posix(path)
|
|
493
|
+
path = path.replace(self.xlat_to_posix(os.path.expanduser('~')), '~')
|
|
494
|
+
path = path.replace('\\', '/')
|
|
495
|
+
return path
|
|
496
|
+
|
|
497
|
+
# -------------------
|
|
498
|
+
## current repo directory converted to posix
|
|
499
|
+
# @return the repo dir path
|
|
500
|
+
@property
|
|
501
|
+
def curr_repo_dir(self):
|
|
502
|
+
if self.ut_curr_dir is None:
|
|
503
|
+
repo_dir = os.getcwd()
|
|
504
|
+
repo_dir = repo_dir.replace('\\', '/')
|
|
505
|
+
repo_dir = repo_dir.replace('//', '/')
|
|
506
|
+
else:
|
|
507
|
+
repo_dir = self.ut_curr_dir
|
|
508
|
+
return repo_dir
|
|
509
|
+
|
|
510
|
+
# -------------------
|
|
511
|
+
## current repo name
|
|
512
|
+
# @return the repo name
|
|
513
|
+
@property
|
|
514
|
+
def curr_repo_name(self):
|
|
515
|
+
return os.path.basename(os.path.normpath(os.getcwd()))
|
|
516
|
+
|
|
517
|
+
# --------------------
|
|
518
|
+
## abort the current script.
|
|
519
|
+
# Note: do not use logging here since it may fail to write correctly
|
|
520
|
+
#
|
|
521
|
+
# @param msg (optional) message to display
|
|
522
|
+
# @return does not return
|
|
523
|
+
def abort(self, msg='abort occurred, exiting'):
|
|
524
|
+
sys.stdout.write('\n')
|
|
525
|
+
sys.stdout.write(f'ABRT {msg}')
|
|
526
|
+
self.stdout_flush()
|
|
527
|
+
self.stderr_flush()
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
|
|
530
|
+
# # -------------------
|
|
531
|
+
# class ExcpAbort(Exception):
|
|
532
|
+
# def __init__(self, message):
|
|
533
|
+
# super().__init__(message)
|
|
534
|
+
# self.level = 'line'
|
|
535
|
+
# self.detail = None
|
|
536
|
+
|
|
537
|
+
# def abort(self, msg='ABRT session aborted', level='line'):
|
|
538
|
+
# excp = ExcpAbort(msg)
|
|
539
|
+
# excp.level = level
|
|
540
|
+
# raise excp
|
|
541
|
+
|
|
542
|
+
# === UT functions
|
|
543
|
+
|
|
544
|
+
# -------------------
|
|
545
|
+
## get ut_mode
|
|
546
|
+
# @return ut_mode (true/false)
|
|
547
|
+
@property
|
|
548
|
+
def ut_mode(self):
|
|
549
|
+
return self._ut_mode
|
|
550
|
+
|
|
551
|
+
# -------------------
|
|
552
|
+
## set for ut_mode
|
|
553
|
+
# @param val new value to use
|
|
554
|
+
# @return None
|
|
555
|
+
@ut_mode.setter
|
|
556
|
+
def ut_mode(self, val):
|
|
557
|
+
self._ut_mode = val
|
|
558
|
+
|
|
559
|
+
# -------------------
|
|
560
|
+
## get set os_name
|
|
561
|
+
# @param ut_os_name new value to use
|
|
562
|
+
# @return ut_mode (true/false)
|
|
563
|
+
def ut_info(self, ut_os_name):
|
|
564
|
+
self._os_name = ut_os_name
|
|
565
|
+
self._set_all_data()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyosal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: python Operating System Abstract Layer
|
|
5
|
+
Author-email: "J. Arrizza" <cppgent0@gmail.com>
|
|
6
|
+
Maintainer-email: "J. Arrizza" <cppgent0@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Website, https://arrizza.com/pyosal
|
|
9
|
+
Project-URL: Source, https://bitbucket.org/arrizza-public/pyosal/src/master
|
|
10
|
+
Project-URL: Download, https://bitbucket.org/arrizza-public/pyosal/get/master.zip
|
|
11
|
+
Keywords: SHOULD-NOT-MATCH,SHOULD-NOT-MATCH,SHOULD-NOT-MATCH
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.txt
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
* website: [Website](https://arrizza.com/TODO.html)
|
|
24
|
+
* installation: [Common Setup](https://arrizza.com/setup-common.html)
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
***Under Construction***
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.cfg
|
|
6
|
+
src/pyosal/__init__.py
|
|
7
|
+
src/pyosal/constants.py
|
|
8
|
+
src/pyosal/constants_version.py
|
|
9
|
+
src/pyosal/pyosal.py
|
|
10
|
+
src/pyosal.egg-info/PKG-INFO
|
|
11
|
+
src/pyosal.egg-info/SOURCES.txt
|
|
12
|
+
src/pyosal.egg-info/dependency_links.txt
|
|
13
|
+
src/pyosal.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyosal
|