python3-cyberfusion-common 2.10.11.1__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.
- python3-cyberfusion-common-2.10.11.1/LICENSE +21 -0
- python3-cyberfusion-common-2.10.11.1/PKG-INFO +59 -0
- python3-cyberfusion-common-2.10.11.1/README.md +40 -0
- python3-cyberfusion-common-2.10.11.1/pyproject.toml +22 -0
- python3-cyberfusion-common-2.10.11.1/setup.cfg +12 -0
- python3-cyberfusion-common-2.10.11.1/setup.py +34 -0
- python3-cyberfusion-common-2.10.11.1/src/cyberfusion/Common/Config.py +41 -0
- python3-cyberfusion-common-2.10.11.1/src/cyberfusion/Common/Filesystem.py +81 -0
- python3-cyberfusion-common-2.10.11.1/src/cyberfusion/Common/FilesystemComparison.py +185 -0
- python3-cyberfusion-common-2.10.11.1/src/cyberfusion/Common/__init__.py +157 -0
- python3-cyberfusion-common-2.10.11.1/src/cyberfusion/Common/exceptions/__init__.py +10 -0
- python3-cyberfusion-common-2.10.11.1/src/python3_cyberfusion_common.egg-info/PKG-INFO +59 -0
- python3-cyberfusion-common-2.10.11.1/src/python3_cyberfusion_common.egg-info/SOURCES.txt +15 -0
- python3-cyberfusion-common-2.10.11.1/src/python3_cyberfusion_common.egg-info/dependency_links.txt +1 -0
- python3-cyberfusion-common-2.10.11.1/src/python3_cyberfusion_common.egg-info/requires.txt +3 -0
- python3-cyberfusion-common-2.10.11.1/src/python3_cyberfusion_common.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Cyberfusion
|
|
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,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python3-cyberfusion-common
|
|
3
|
+
Version: 2.10.11.1
|
|
4
|
+
Summary: Common utilities.
|
|
5
|
+
Home-page: https://vcs.cyberfusion.nl/shared/python3-cyberfusion-common
|
|
6
|
+
Author: William Edwards
|
|
7
|
+
Author-email: wedwards@cyberfusion.nl
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: cyberfusion,common
|
|
10
|
+
Platform: linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# python3-cyberfusion-common
|
|
19
|
+
|
|
20
|
+
Common utilities.
|
|
21
|
+
|
|
22
|
+
# Install
|
|
23
|
+
|
|
24
|
+
## PyPI
|
|
25
|
+
|
|
26
|
+
Run the following command to install the package from PyPI:
|
|
27
|
+
|
|
28
|
+
pip3 install python3-cyberfusion-common
|
|
29
|
+
|
|
30
|
+
## Generic
|
|
31
|
+
|
|
32
|
+
Run the following command to create a source distribution:
|
|
33
|
+
|
|
34
|
+
python3 setup.py sdist
|
|
35
|
+
|
|
36
|
+
## Debian
|
|
37
|
+
|
|
38
|
+
Run the following commands to build a Debian package:
|
|
39
|
+
|
|
40
|
+
mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
|
|
41
|
+
dpkg-buildpackage -us -uc
|
|
42
|
+
|
|
43
|
+
# Configure
|
|
44
|
+
|
|
45
|
+
No configuration is supported.
|
|
46
|
+
|
|
47
|
+
# Usage
|
|
48
|
+
|
|
49
|
+
See code.
|
|
50
|
+
|
|
51
|
+
# Tests
|
|
52
|
+
|
|
53
|
+
Run tests with pytest:
|
|
54
|
+
|
|
55
|
+
pytest tests/
|
|
56
|
+
|
|
57
|
+
The tests must be run from the project root.
|
|
58
|
+
|
|
59
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# python3-cyberfusion-common
|
|
2
|
+
|
|
3
|
+
Common utilities.
|
|
4
|
+
|
|
5
|
+
# Install
|
|
6
|
+
|
|
7
|
+
## PyPI
|
|
8
|
+
|
|
9
|
+
Run the following command to install the package from PyPI:
|
|
10
|
+
|
|
11
|
+
pip3 install python3-cyberfusion-common
|
|
12
|
+
|
|
13
|
+
## Generic
|
|
14
|
+
|
|
15
|
+
Run the following command to create a source distribution:
|
|
16
|
+
|
|
17
|
+
python3 setup.py sdist
|
|
18
|
+
|
|
19
|
+
## Debian
|
|
20
|
+
|
|
21
|
+
Run the following commands to build a Debian package:
|
|
22
|
+
|
|
23
|
+
mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
|
|
24
|
+
dpkg-buildpackage -us -uc
|
|
25
|
+
|
|
26
|
+
# Configure
|
|
27
|
+
|
|
28
|
+
No configuration is supported.
|
|
29
|
+
|
|
30
|
+
# Usage
|
|
31
|
+
|
|
32
|
+
See code.
|
|
33
|
+
|
|
34
|
+
# Tests
|
|
35
|
+
|
|
36
|
+
Run tests with pytest:
|
|
37
|
+
|
|
38
|
+
pytest tests/
|
|
39
|
+
|
|
40
|
+
The tests must be run from the project root.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[tool.isort]
|
|
2
|
+
profile = "black"
|
|
3
|
+
line_length = 79
|
|
4
|
+
known_first_party = ["cyberfusion"]
|
|
5
|
+
default_section = "THIRDPARTY"
|
|
6
|
+
|
|
7
|
+
[tool.black]
|
|
8
|
+
line-length = 79
|
|
9
|
+
target-version = ["py37"]
|
|
10
|
+
exclude = '''
|
|
11
|
+
(
|
|
12
|
+
/(
|
|
13
|
+
\.eggs # exclude a few common directories in the
|
|
14
|
+
| \.git # root of the project
|
|
15
|
+
| \.hg
|
|
16
|
+
| \.mypy_cache
|
|
17
|
+
| \.tox
|
|
18
|
+
| \.venv
|
|
19
|
+
| venv
|
|
20
|
+
)/
|
|
21
|
+
)
|
|
22
|
+
'''
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""A setuptools based setup module."""
|
|
2
|
+
|
|
3
|
+
from setuptools import setup
|
|
4
|
+
|
|
5
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
6
|
+
long_description = fh.read()
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name="python3-cyberfusion-common",
|
|
10
|
+
version="2.10.11.1",
|
|
11
|
+
description="Common utilities.",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
python_requires=">=3.9",
|
|
15
|
+
author="William Edwards",
|
|
16
|
+
author_email="wedwards@cyberfusion.nl",
|
|
17
|
+
url="https://vcs.cyberfusion.nl/shared/python3-cyberfusion-common",
|
|
18
|
+
platforms=["linux"],
|
|
19
|
+
packages=["cyberfusion.Common", "cyberfusion.Common.exceptions"],
|
|
20
|
+
package_dir={"": "src"},
|
|
21
|
+
data_files=[],
|
|
22
|
+
install_requires=[
|
|
23
|
+
"cached_property==1.5.2",
|
|
24
|
+
"psutil==5.8.0",
|
|
25
|
+
"requests==2.25.1",
|
|
26
|
+
],
|
|
27
|
+
classifiers=[
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
],
|
|
32
|
+
keywords=["cyberfusion", "common"],
|
|
33
|
+
license="MIT",
|
|
34
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Helper for handling config files."""
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from cached_property import cached_property
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CyberfusionConfig:
|
|
11
|
+
"""Abstract ConfigParser implementation for use in scripts."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
path: Optional[str] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Set attributes."""
|
|
18
|
+
self._path = path
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def path(self) -> str:
|
|
22
|
+
"""Set config file path."""
|
|
23
|
+
if not self._path:
|
|
24
|
+
self._path = os.path.join(
|
|
25
|
+
os.path.sep, "etc", "cyberfusion", "cyberfusion.cfg"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return self._path
|
|
29
|
+
|
|
30
|
+
@cached_property
|
|
31
|
+
def config(self) -> configparser.ConfigParser:
|
|
32
|
+
"""Read config."""
|
|
33
|
+
config = configparser.ConfigParser()
|
|
34
|
+
|
|
35
|
+
config.read(self.path)
|
|
36
|
+
|
|
37
|
+
return config
|
|
38
|
+
|
|
39
|
+
def get(self, section: str, key: str) -> str:
|
|
40
|
+
"""Retrieve config option."""
|
|
41
|
+
return self.config.get(section, key)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Helper for handling filesystem."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import psutil
|
|
8
|
+
|
|
9
|
+
CEPH_NAME_ATTRIBUTE_RBYTES = "ceph.dir.rbytes"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FilesystemType(Enum):
|
|
13
|
+
"""Filesystem types.
|
|
14
|
+
|
|
15
|
+
We don't expect any other filesystems than the ones in this Enum under normal
|
|
16
|
+
circumstances. This Enum prevents you from having to write filesystem types
|
|
17
|
+
by name manually; it's not a complete list of all possibilities.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
EXT4 = "ext4"
|
|
21
|
+
EXT2 = "ext2" # Usually for /boot
|
|
22
|
+
XFS = "xfs"
|
|
23
|
+
CEPH = "ceph"
|
|
24
|
+
OVERLAY = "overlay" # Usually for Docker
|
|
25
|
+
APFS = "apfs" # Development on macOS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_filesystem(path: str) -> str:
|
|
29
|
+
"""Get filesystem that absolute path is on.
|
|
30
|
+
|
|
31
|
+
Returns the earliest filesystem. E.g. if '/a/b/c' is passed, and both '/a' and
|
|
32
|
+
'/b' are mountpoints, '/b' is returned.
|
|
33
|
+
|
|
34
|
+
Does not resolve symlinks, so the filesystem of the symlink itself is looked
|
|
35
|
+
up.
|
|
36
|
+
|
|
37
|
+
Inspired by https://stackoverflow.com/a/4453715/3837431
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Ensure path is an absolute path. For relative paths, dirname() would return
|
|
41
|
+
# the wrong result
|
|
42
|
+
|
|
43
|
+
path = os.path.abspath(path)
|
|
44
|
+
|
|
45
|
+
# Work our way down the path by getting the directory that the current
|
|
46
|
+
# filesystem object is in
|
|
47
|
+
|
|
48
|
+
while not os.path.ismount(path):
|
|
49
|
+
path = os.path.dirname(path)
|
|
50
|
+
|
|
51
|
+
return path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_filesystem_type(path: str) -> FilesystemType:
|
|
55
|
+
"""Get type of filesystem."""
|
|
56
|
+
|
|
57
|
+
# If this yields no results (i.e. the path is missing), next() raises StopIteration
|
|
58
|
+
|
|
59
|
+
partition = next(
|
|
60
|
+
filter(
|
|
61
|
+
lambda x: x.mountpoint == path, psutil.disk_partitions(all=True)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return FilesystemType(partition.fstype)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_directory_size(path: str) -> int:
|
|
69
|
+
"""Get size of directory."""
|
|
70
|
+
|
|
71
|
+
# CephFS is a special case, as CephFS has extended attributes to determine
|
|
72
|
+
# size
|
|
73
|
+
|
|
74
|
+
is_ceph = get_filesystem_type(get_filesystem(path)) == FilesystemType.CEPH
|
|
75
|
+
|
|
76
|
+
if is_ceph:
|
|
77
|
+
return int(os.getxattr(path, CEPH_NAME_ATTRIBUTE_RBYTES).decode("utf-8")) # type: ignore[attr-defined]
|
|
78
|
+
|
|
79
|
+
# Loop through directory for filesystems without special implementation
|
|
80
|
+
|
|
81
|
+
return sum(f.stat().st_size for f in Path(path).rglob("*") if f.is_file())
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Helper for comparing filesystem objects."""
|
|
2
|
+
|
|
3
|
+
import filecmp
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_recursive_dircmps(dircmp: filecmp.dircmp) -> List[filecmp.dircmp]:
|
|
10
|
+
"""Get dircmp object for every subdirectory.
|
|
11
|
+
|
|
12
|
+
Subdirectories are only returned for directories that are in the left and
|
|
13
|
+
right directories.
|
|
14
|
+
"""
|
|
15
|
+
dircmps = []
|
|
16
|
+
|
|
17
|
+
dircmps.append(dircmp)
|
|
18
|
+
|
|
19
|
+
for sub_dircmp in dircmp.subdirs.values():
|
|
20
|
+
dircmps.extend(_get_recursive_dircmps(sub_dircmp))
|
|
21
|
+
|
|
22
|
+
return dircmps
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_different_files_in_directories(
|
|
26
|
+
left_directory_path: str, right_directory_path: str
|
|
27
|
+
) -> List[Tuple[str, str]]:
|
|
28
|
+
"""Get files with same name in both directories but with different contents or attributes.
|
|
29
|
+
|
|
30
|
+
Files which are present in subdirectories that only exist in either the left
|
|
31
|
+
or right directories are not returned. Use the 'get_directories_only_in_*_directory'
|
|
32
|
+
functions to get subdirectories that are in either the left or right directories.
|
|
33
|
+
This ensures that this function does not return individual files in whole new
|
|
34
|
+
directories.
|
|
35
|
+
"""
|
|
36
|
+
dircmps = _get_recursive_dircmps(
|
|
37
|
+
filecmp.dircmp(left_directory_path, right_directory_path)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
files = []
|
|
41
|
+
|
|
42
|
+
for dircmp in dircmps:
|
|
43
|
+
for diff_file in dircmp.diff_files:
|
|
44
|
+
absolute_path_left = os.path.join(dircmp.left, diff_file)
|
|
45
|
+
absolute_path_right = os.path.join(dircmp.right, diff_file)
|
|
46
|
+
|
|
47
|
+
files.append((absolute_path_left, absolute_path_right))
|
|
48
|
+
|
|
49
|
+
return files
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_files_only_in_left_directory(
|
|
53
|
+
left_directory_path: str, right_directory_path: str
|
|
54
|
+
) -> List[str]:
|
|
55
|
+
"""Get files which are only present in left directory.
|
|
56
|
+
|
|
57
|
+
Files which are present in subdirectories that only exist in either the left
|
|
58
|
+
or right directories are not returned. Use the 'get_directories_only_in_*_directory'
|
|
59
|
+
functions to get subdirectories that are in either the left or right directories.
|
|
60
|
+
This ensures that this function does not return individual files in whole new
|
|
61
|
+
directories.
|
|
62
|
+
"""
|
|
63
|
+
dircmps = _get_recursive_dircmps(
|
|
64
|
+
filecmp.dircmp(left_directory_path, right_directory_path)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
files = []
|
|
68
|
+
|
|
69
|
+
for dircmp in dircmps:
|
|
70
|
+
for left_only_file in dircmp.left_only:
|
|
71
|
+
absolute_path = os.path.join(dircmp.left, left_only_file)
|
|
72
|
+
|
|
73
|
+
if os.path.isdir(absolute_path):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
files.append(absolute_path)
|
|
77
|
+
|
|
78
|
+
return files
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_files_only_in_right_directory(
|
|
82
|
+
left_directory_path: str, right_directory_path: str
|
|
83
|
+
) -> List[str]:
|
|
84
|
+
"""Get files which are only present in right directory.
|
|
85
|
+
|
|
86
|
+
Files which are present in subdirectories that only exist in either the left
|
|
87
|
+
or right directories are not returned. Use the 'get_directories_only_in_*_directory'
|
|
88
|
+
functions to get subdirectories that are in either the left or right directories.
|
|
89
|
+
This ensures that this function does not return individual files in whole new
|
|
90
|
+
directories.
|
|
91
|
+
"""
|
|
92
|
+
dircmps = _get_recursive_dircmps(
|
|
93
|
+
filecmp.dircmp(left_directory_path, right_directory_path)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
files = []
|
|
97
|
+
|
|
98
|
+
for dircmp in dircmps:
|
|
99
|
+
for right_only_file in dircmp.right_only:
|
|
100
|
+
absolute_path = os.path.join(dircmp.right, right_only_file)
|
|
101
|
+
|
|
102
|
+
if os.path.isdir(absolute_path):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
files.append(absolute_path)
|
|
106
|
+
|
|
107
|
+
return files
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_directories_only_in_left_directory(
|
|
111
|
+
left_directory_path: str, right_directory_path: str
|
|
112
|
+
) -> List[str]:
|
|
113
|
+
"""Get directories which are only present in left directory."""
|
|
114
|
+
dircmps = _get_recursive_dircmps(
|
|
115
|
+
filecmp.dircmp(left_directory_path, right_directory_path)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
directories = []
|
|
119
|
+
|
|
120
|
+
for dircmp in dircmps:
|
|
121
|
+
for left_only_file in dircmp.left_only:
|
|
122
|
+
absolute_path = os.path.join(dircmp.left, left_only_file)
|
|
123
|
+
|
|
124
|
+
if os.path.isfile(absolute_path):
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
directories.append(absolute_path)
|
|
128
|
+
|
|
129
|
+
return directories
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_directories_only_in_right_directory(
|
|
133
|
+
left_directory_path: str, right_directory_path: str
|
|
134
|
+
) -> List[str]:
|
|
135
|
+
"""Get directories which are only present in right directory."""
|
|
136
|
+
dircmps = _get_recursive_dircmps(
|
|
137
|
+
filecmp.dircmp(left_directory_path, right_directory_path)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
directories = []
|
|
141
|
+
|
|
142
|
+
for dircmp in dircmps:
|
|
143
|
+
for right_only_file in dircmp.right_only:
|
|
144
|
+
absolute_path = os.path.join(dircmp.right, right_only_file)
|
|
145
|
+
|
|
146
|
+
if os.path.isfile(absolute_path):
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
directories.append(absolute_path)
|
|
150
|
+
|
|
151
|
+
return directories
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_nested_directory_structure(
|
|
155
|
+
paths_list: List[str],
|
|
156
|
+
) -> Dict[str, Optional[Dict[str, Optional[dict]]]]:
|
|
157
|
+
"""Convert list of paths to nested dict."""
|
|
158
|
+
tree: Dict[str, Optional[Dict[str, Optional[dict]]]] = {}
|
|
159
|
+
|
|
160
|
+
# Example of logic:
|
|
161
|
+
#
|
|
162
|
+
# >>> tree = {}
|
|
163
|
+
# >>> node = tree
|
|
164
|
+
# >>> node = node.setdefault('a', {})
|
|
165
|
+
# >>> node = node.setdefault('b', {})
|
|
166
|
+
# >>> node = tree
|
|
167
|
+
# >>> tree
|
|
168
|
+
# {'a': {'b': {}}}
|
|
169
|
+
|
|
170
|
+
for path in paths_list:
|
|
171
|
+
node = tree # Reset to root level for new path, see docstring
|
|
172
|
+
|
|
173
|
+
parts = Path(path).parts
|
|
174
|
+
|
|
175
|
+
for i, element in enumerate(parts):
|
|
176
|
+
last = i == (len(parts) - 1)
|
|
177
|
+
|
|
178
|
+
if last:
|
|
179
|
+
node.setdefault(element, None)
|
|
180
|
+
else:
|
|
181
|
+
node = node.setdefault( # type: ignore[assignment] # mypy does not support nested types
|
|
182
|
+
element, {}
|
|
183
|
+
) # Next recursion will be at this level, see docstring
|
|
184
|
+
|
|
185
|
+
return tree
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Helper classes for Cyberfusion scripts."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import shutil
|
|
7
|
+
import string
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from hashlib import md5, sha1
|
|
11
|
+
from socket import gethostname
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from cyberfusion.Common.exceptions import ExecutableNotFound
|
|
17
|
+
|
|
18
|
+
CHAR_PREFIX_WILDCARD = "*"
|
|
19
|
+
CHAR_LABEL = "."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EmailAddresses:
|
|
23
|
+
"""Cyberfusion email addresses."""
|
|
24
|
+
|
|
25
|
+
ENGINEERING = "engineering@cyberfusion.nl"
|
|
26
|
+
SUPPORT = "support@cyberfusion.nl"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_executable(name: str) -> str:
|
|
30
|
+
"""Find absolute path of executable.
|
|
31
|
+
|
|
32
|
+
Use this function when the executable must exist. This function raises an
|
|
33
|
+
exception if it does not.
|
|
34
|
+
"""
|
|
35
|
+
path = shutil.which(name)
|
|
36
|
+
|
|
37
|
+
if path:
|
|
38
|
+
return path
|
|
39
|
+
|
|
40
|
+
raise ExecutableNotFound(name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def try_find_executable(name: str) -> Optional[str]:
|
|
44
|
+
"""Find absolute path of executable.
|
|
45
|
+
|
|
46
|
+
Use this function when the executable may be absent. This function returns
|
|
47
|
+
None if it is.
|
|
48
|
+
"""
|
|
49
|
+
return shutil.which(name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def download_from_url(
|
|
53
|
+
url: str, *, root_directory: Optional[str] = None
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Download from URL.
|
|
56
|
+
|
|
57
|
+
Large files are supported due to use of chunking and streaming.
|
|
58
|
+
|
|
59
|
+
Inspired by https://stackoverflow.com/a/16696317/19535769
|
|
60
|
+
"""
|
|
61
|
+
if root_directory is None:
|
|
62
|
+
root_directory = os.path.join(os.path.sep, "tmp")
|
|
63
|
+
|
|
64
|
+
path = os.path.join(root_directory, generate_random_string())
|
|
65
|
+
|
|
66
|
+
# Create and set permissions
|
|
67
|
+
|
|
68
|
+
with open(path, "w"):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
os.chmod(path, 0o600)
|
|
72
|
+
|
|
73
|
+
# Download
|
|
74
|
+
|
|
75
|
+
with requests.get(url, stream=True) as r:
|
|
76
|
+
r.raise_for_status()
|
|
77
|
+
|
|
78
|
+
with open(path, "wb") as f:
|
|
79
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
80
|
+
f.write(chunk)
|
|
81
|
+
|
|
82
|
+
return path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_tmp_file() -> str:
|
|
86
|
+
"""Create tmp file and return path."""
|
|
87
|
+
path = os.path.join(os.path.sep, "tmp", str(uuid.uuid4()))
|
|
88
|
+
|
|
89
|
+
with open(path, "w"):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
os.chmod(path, 0o600) # Do not allow regular users to view file contents
|
|
93
|
+
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_hostname() -> str:
|
|
98
|
+
"""Get hostname."""
|
|
99
|
+
return gethostname()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_domain_is_wildcard(domain: str) -> bool:
|
|
103
|
+
"""Determine if domain is wildcard."""
|
|
104
|
+
return domain.split(CHAR_LABEL)[0] == CHAR_PREFIX_WILDCARD
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_today_timestamp() -> float:
|
|
108
|
+
"""Get UNIX timestamp from the first second of today."""
|
|
109
|
+
current_datetime = datetime.utcnow()
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
datetime(
|
|
113
|
+
year=current_datetime.year,
|
|
114
|
+
month=current_datetime.month,
|
|
115
|
+
day=current_datetime.day,
|
|
116
|
+
)
|
|
117
|
+
.replace(tzinfo=timezone.utc)
|
|
118
|
+
.timestamp()
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def generate_random_string(length: int = 24) -> str:
|
|
123
|
+
"""Generate random string."""
|
|
124
|
+
|
|
125
|
+
# Set allowed characters (ASCII + digits)
|
|
126
|
+
|
|
127
|
+
alphabet = string.ascii_letters + string.digits
|
|
128
|
+
|
|
129
|
+
# Return string using allowed characters
|
|
130
|
+
|
|
131
|
+
return "".join(secrets.choice(alphabet) for i in range(length))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def hash_string_mariadb(string: str) -> str:
|
|
135
|
+
"""Hash string with SHA-1 by MariaDB standards."""
|
|
136
|
+
return (
|
|
137
|
+
"*"
|
|
138
|
+
+ sha1(sha1(string.encode("utf-8")).digest()) # noqa: S303
|
|
139
|
+
.hexdigest()
|
|
140
|
+
.upper()
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def convert_bytes_gib(size: int) -> float:
|
|
145
|
+
"""Convert bytes to GiB."""
|
|
146
|
+
return size / (1024**3)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_md5_hash(path: str) -> str:
|
|
150
|
+
"""Get Base64 encoded 128-bit MD5 digest of file."""
|
|
151
|
+
hash_ = md5()
|
|
152
|
+
|
|
153
|
+
with open(path, "rb") as f:
|
|
154
|
+
for chunk in iter(lambda: f.read(128 * hash_.block_size), b""):
|
|
155
|
+
hash_.update(chunk)
|
|
156
|
+
|
|
157
|
+
return base64.b64encode(hash_.digest()).decode()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python3-cyberfusion-common
|
|
3
|
+
Version: 2.10.11.1
|
|
4
|
+
Summary: Common utilities.
|
|
5
|
+
Home-page: https://vcs.cyberfusion.nl/shared/python3-cyberfusion-common
|
|
6
|
+
Author: William Edwards
|
|
7
|
+
Author-email: wedwards@cyberfusion.nl
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: cyberfusion,common
|
|
10
|
+
Platform: linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# python3-cyberfusion-common
|
|
19
|
+
|
|
20
|
+
Common utilities.
|
|
21
|
+
|
|
22
|
+
# Install
|
|
23
|
+
|
|
24
|
+
## PyPI
|
|
25
|
+
|
|
26
|
+
Run the following command to install the package from PyPI:
|
|
27
|
+
|
|
28
|
+
pip3 install python3-cyberfusion-common
|
|
29
|
+
|
|
30
|
+
## Generic
|
|
31
|
+
|
|
32
|
+
Run the following command to create a source distribution:
|
|
33
|
+
|
|
34
|
+
python3 setup.py sdist
|
|
35
|
+
|
|
36
|
+
## Debian
|
|
37
|
+
|
|
38
|
+
Run the following commands to build a Debian package:
|
|
39
|
+
|
|
40
|
+
mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
|
|
41
|
+
dpkg-buildpackage -us -uc
|
|
42
|
+
|
|
43
|
+
# Configure
|
|
44
|
+
|
|
45
|
+
No configuration is supported.
|
|
46
|
+
|
|
47
|
+
# Usage
|
|
48
|
+
|
|
49
|
+
See code.
|
|
50
|
+
|
|
51
|
+
# Tests
|
|
52
|
+
|
|
53
|
+
Run tests with pytest:
|
|
54
|
+
|
|
55
|
+
pytest tests/
|
|
56
|
+
|
|
57
|
+
The tests must be run from the project root.
|
|
58
|
+
|
|
59
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
src/cyberfusion/Common/Config.py
|
|
7
|
+
src/cyberfusion/Common/Filesystem.py
|
|
8
|
+
src/cyberfusion/Common/FilesystemComparison.py
|
|
9
|
+
src/cyberfusion/Common/__init__.py
|
|
10
|
+
src/cyberfusion/Common/exceptions/__init__.py
|
|
11
|
+
src/python3_cyberfusion_common.egg-info/PKG-INFO
|
|
12
|
+
src/python3_cyberfusion_common.egg-info/SOURCES.txt
|
|
13
|
+
src/python3_cyberfusion_common.egg-info/dependency_links.txt
|
|
14
|
+
src/python3_cyberfusion_common.egg-info/requires.txt
|
|
15
|
+
src/python3_cyberfusion_common.egg-info/top_level.txt
|
python3-cyberfusion-common-2.10.11.1/src/python3_cyberfusion_common.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cyberfusion
|