python3-cyberfusion-common 2.10.11.3.3__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.
- cyberfusion/Common/Config.py +42 -0
- cyberfusion/Common/Filesystem.py +79 -0
- cyberfusion/Common/FilesystemComparison.py +185 -0
- cyberfusion/Common/__init__.py +156 -0
- cyberfusion/Common/exceptions/__init__.py +10 -0
- python3_cyberfusion_common-2.10.11.3.3.dist-info/METADATA +39 -0
- python3_cyberfusion_common-2.10.11.3.3.dist-info/RECORD +9 -0
- python3_cyberfusion_common-2.10.11.3.3.dist-info/WHEEL +5 -0
- python3_cyberfusion_common-2.10.11.3.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
with open(self.path, "r") as f:
|
|
36
|
+
config.read_file(f)
|
|
37
|
+
|
|
38
|
+
return config
|
|
39
|
+
|
|
40
|
+
def get(self, section: str, key: str) -> str:
|
|
41
|
+
"""Retrieve config option."""
|
|
42
|
+
return self.config.get(section, key)
|
|
@@ -0,0 +1,79 @@
|
|
|
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(lambda x: x.mountpoint == path, psutil.disk_partitions(all=True))
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return FilesystemType(partition.fstype)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_directory_size(path: str) -> int:
|
|
67
|
+
"""Get size of directory."""
|
|
68
|
+
|
|
69
|
+
# CephFS is a special case, as CephFS has extended attributes to determine
|
|
70
|
+
# size
|
|
71
|
+
|
|
72
|
+
is_ceph = get_filesystem_type(get_filesystem(path)) == FilesystemType.CEPH
|
|
73
|
+
|
|
74
|
+
if is_ceph:
|
|
75
|
+
return int(os.getxattr(path, CEPH_NAME_ATTRIBUTE_RBYTES).decode("utf-8")) # type: ignore[attr-defined]
|
|
76
|
+
|
|
77
|
+
# Loop through directory for filesystems without special implementation
|
|
78
|
+
|
|
79
|
+
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,156 @@
|
|
|
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
|
+
SYSTEM_MESSAGES_CORE = "system-messages.core@cyberfusion.io"
|
|
26
|
+
SYSTEM_MESSAGES_INFRASTRUCTURE = "system-messages.infrastructure@cyberfusion.io"
|
|
27
|
+
SUPPORT = "support@cyberfusion.io"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_executable(name: str) -> str:
|
|
31
|
+
"""Find absolute path of executable.
|
|
32
|
+
|
|
33
|
+
Use this function when the executable must exist. This function raises an
|
|
34
|
+
exception if it does not.
|
|
35
|
+
"""
|
|
36
|
+
path = shutil.which(name)
|
|
37
|
+
|
|
38
|
+
if path:
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
raise ExecutableNotFound(name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def try_find_executable(name: str) -> Optional[str]:
|
|
45
|
+
"""Find absolute path of executable.
|
|
46
|
+
|
|
47
|
+
Use this function when the executable may be absent. This function returns
|
|
48
|
+
None if it is.
|
|
49
|
+
"""
|
|
50
|
+
return shutil.which(name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def download_from_url(url: str, *, root_directory: Optional[str] = None) -> str:
|
|
54
|
+
"""Download from URL.
|
|
55
|
+
|
|
56
|
+
Large files are supported due to use of chunking and streaming.
|
|
57
|
+
|
|
58
|
+
Inspired by https://stackoverflow.com/a/16696317/19535769
|
|
59
|
+
"""
|
|
60
|
+
if root_directory is None:
|
|
61
|
+
root_directory = os.path.join(os.path.sep, "tmp")
|
|
62
|
+
|
|
63
|
+
path = os.path.join(root_directory, generate_random_string())
|
|
64
|
+
|
|
65
|
+
# Create and set permissions
|
|
66
|
+
|
|
67
|
+
with open(path, "w"):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
os.chmod(path, 0o600)
|
|
71
|
+
|
|
72
|
+
# Download
|
|
73
|
+
|
|
74
|
+
with requests.get(url, stream=True) as r:
|
|
75
|
+
r.raise_for_status()
|
|
76
|
+
|
|
77
|
+
with open(path, "wb") as f:
|
|
78
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
79
|
+
f.write(chunk)
|
|
80
|
+
|
|
81
|
+
return path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_tmp_file() -> str:
|
|
85
|
+
"""Create tmp file and return path."""
|
|
86
|
+
path = os.path.join(os.path.sep, "tmp", str(uuid.uuid4()))
|
|
87
|
+
|
|
88
|
+
with open(path, "w"):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
os.chmod(path, 0o600) # Do not allow regular users to view file contents
|
|
92
|
+
|
|
93
|
+
return path
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_hostname() -> str:
|
|
97
|
+
"""Get hostname."""
|
|
98
|
+
return gethostname()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_domain_is_wildcard(domain: str) -> bool:
|
|
102
|
+
"""Determine if domain is wildcard."""
|
|
103
|
+
return domain.split(CHAR_LABEL)[0] == CHAR_PREFIX_WILDCARD
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_today_timestamp() -> float:
|
|
107
|
+
"""Get UNIX timestamp from the first second of today."""
|
|
108
|
+
current_datetime = datetime.utcnow()
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
datetime(
|
|
112
|
+
year=current_datetime.year,
|
|
113
|
+
month=current_datetime.month,
|
|
114
|
+
day=current_datetime.day,
|
|
115
|
+
)
|
|
116
|
+
.replace(tzinfo=timezone.utc)
|
|
117
|
+
.timestamp()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def generate_random_string(length: int = 24) -> str:
|
|
122
|
+
"""Generate random string."""
|
|
123
|
+
|
|
124
|
+
# Set allowed characters (ASCII + digits)
|
|
125
|
+
|
|
126
|
+
alphabet = string.ascii_letters + string.digits
|
|
127
|
+
|
|
128
|
+
# Return string using allowed characters
|
|
129
|
+
|
|
130
|
+
return "".join(secrets.choice(alphabet) for i in range(length))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def hash_string_mariadb(string: str) -> str:
|
|
134
|
+
"""Hash string with SHA-1 by MariaDB standards."""
|
|
135
|
+
return (
|
|
136
|
+
"*"
|
|
137
|
+
+ sha1(sha1(string.encode("utf-8")).digest()) # noqa: S303
|
|
138
|
+
.hexdigest()
|
|
139
|
+
.upper()
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def convert_bytes_gib(size: int) -> float:
|
|
144
|
+
"""Convert bytes to GiB."""
|
|
145
|
+
return size / (1024**3)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_md5_hash(path: str) -> str:
|
|
149
|
+
"""Get Base64 encoded 128-bit MD5 digest of file."""
|
|
150
|
+
hash_ = md5()
|
|
151
|
+
|
|
152
|
+
with open(path, "rb") as f:
|
|
153
|
+
for chunk in iter(lambda: f.read(128 * hash_.block_size), b""):
|
|
154
|
+
hash_.update(chunk)
|
|
155
|
+
|
|
156
|
+
return base64.b64encode(hash_.digest()).decode()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python3-cyberfusion-common
|
|
3
|
+
Version: 2.10.11.3.3
|
|
4
|
+
Summary: Common utilities.
|
|
5
|
+
Author-email: Cyberfusion <support@cyberfusion.io>
|
|
6
|
+
Project-URL: Source, https://github.com/CyberfusionIO/python3-cyberfusion-common
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: cached-property ==1.5.2
|
|
9
|
+
Requires-Dist: psutil ==5.9.4
|
|
10
|
+
Requires-Dist: requests ==2.28.1
|
|
11
|
+
|
|
12
|
+
# python3-cyberfusion-common
|
|
13
|
+
|
|
14
|
+
Common utilities.
|
|
15
|
+
|
|
16
|
+
These utilities are mostly used by Cyberfusion libraries, but are also useful standalone.
|
|
17
|
+
|
|
18
|
+
# Install
|
|
19
|
+
|
|
20
|
+
## PyPI
|
|
21
|
+
|
|
22
|
+
Run the following command to install the package from PyPI:
|
|
23
|
+
|
|
24
|
+
pip3 install python3-cyberfusion-common
|
|
25
|
+
|
|
26
|
+
## Debian
|
|
27
|
+
|
|
28
|
+
Run the following commands to build a Debian package:
|
|
29
|
+
|
|
30
|
+
mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
|
|
31
|
+
dpkg-buildpackage -us -uc
|
|
32
|
+
|
|
33
|
+
# Configure
|
|
34
|
+
|
|
35
|
+
No configuration is supported.
|
|
36
|
+
|
|
37
|
+
# Usage
|
|
38
|
+
|
|
39
|
+
See code.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cyberfusion/Common/Config.py,sha256=RwIg8hAibvuep3ZI00DWqh6xD45TaWgS8YYMXNea54E,1049
|
|
2
|
+
cyberfusion/Common/Filesystem.py,sha256=jjBnbOB75QDnEzAaLNEAR0ZIVw7osqe_7SGbOjnAeJQ,2215
|
|
3
|
+
cyberfusion/Common/FilesystemComparison.py,sha256=Tb7d31hLS3WxCTTCGs75FFjVZXvvYAbEij1bnzG-ju0,5646
|
|
4
|
+
cyberfusion/Common/__init__.py,sha256=HxM0gA8Y8BDwVTzqM16W3CnihBzBwgWbszVjjAYaUIg,3730
|
|
5
|
+
cyberfusion/Common/exceptions/__init__.py,sha256=4V0jEBEQwm4uJPDNvgZ0-O5-kNi_hi4TF42icJT8sFo,171
|
|
6
|
+
python3_cyberfusion_common-2.10.11.3.3.dist-info/METADATA,sha256=YaUGY3aQiDUVto7dfM8dlW3O0i85ettnB6bRfuCE2fA,895
|
|
7
|
+
python3_cyberfusion_common-2.10.11.3.3.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
|
|
8
|
+
python3_cyberfusion_common-2.10.11.3.3.dist-info/top_level.txt,sha256=ss011q9S6SL_KIIyq7iujFmIYa0grSjlnInO7cDkeag,12
|
|
9
|
+
python3_cyberfusion_common-2.10.11.3.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cyberfusion
|