ctbl 0.1.11__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.
ctbl-0.1.11/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ **/.vscode
2
+ dist/
3
+ build.txt
4
+ .git
5
+ **/__pycache__
6
+ src/ctbl_tools/prob.py
7
+ bin/
8
+ lib/
9
+ lib64
10
+ pyvenv.cfg
11
+ *.code-workspace
ctbl-0.1.11/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Cristian A. Bravo Lillo, cristian.bravo@gmail.com
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.
ctbl-0.1.11/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctbl
3
+ Version: 0.1.11
4
+ Summary: A set of helpers classes for daily tasks.
5
+ Author-email: Cristian Bravo Lillo <cristian.bravo@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+
17
+ # ctbl_tools: miscelaneous daily tasks helpers
18
+
19
+ A package with miscelaneous classes meant to help at coding console helpers. Currently there are three classes:
20
+
21
+ 1. **config**: it implements a config file that saves itself. It is a specialization of [configparser](https://docs.python.org/3/library/configparser.html).
22
+ 2. **process**: it runs external processes, it saves the return code, and it helps (a little bit) to parse the standard output (or the standard error).
23
+ 3. **git**: a simple interface for git commands.
24
+
25
+ ## config: a self-preserving config file
26
+
27
+ It implements a config file that saves itself.
28
+
29
+ The idea is: we want to use a config file for an application, which is a simple text file that contains pairs of values, in a similar fashion to an INI file. If the file exists, it is used; if it doesn't, it is created. We use [configparser](https://docs.python.org/3/library/configparser.html) for this.
30
+
31
+ An example:
32
+
33
+ ```python
34
+ from ctbl_tools.config import config
35
+
36
+ cfg = config(initpath = '~/somewhere/blabla.ini')
37
+ cfg.create_section('my_configuration')
38
+ cfg.set('my_configuration', 'x', 1)
39
+ cfg.set('my_configuration', 'y', '2')
40
+ ```
41
+
42
+ When the program above ends, it will save all created configuration to the file ~/somewhere/blabla.ini.
43
+
44
+ ## process: a helper to run external programs
45
+
46
+ It runs an external program, and it stores the return code and both the standard output and standard error.
47
+
48
+ You first create an object of this type:
49
+ ```python
50
+ p = process()
51
+ ```
52
+
53
+ Then run a command:
54
+ ```python
55
+ p.run("whoami")
56
+ ```
57
+
58
+ Immediately after you gain access to the return code, stdout and stderr:
59
+ ```python
60
+ print(p.returncode)
61
+ for line in p.stdout:
62
+ print(line)
63
+ ```
64
+
65
+ You may check if return code was 0 (i.e., all good) by checking `p.is_ok()` to be true. You may also check whether there is any stdout or any stderr with `p.is_there_stdout()` and with `p.is_there_stderr()`, respectively.
66
+
67
+ Finally you may extract information out of stdout by using `extract()`. This method receives a regular expression and a boolean that is true by default (meaning that you want to inspect stdout; if false it will inspect stderr instead). The method will look for all matches in each line of stdout and store them in a list. The list stores as many elements as lines stdout has, and each element is a list of all matches within the corresponding line.
68
+
69
+ ## git: a simple git interface
70
+
71
+ It provides a simple programmatical interface to git.
72
+
73
+ First, you create a git object by providing a local path to a repo:
74
+
75
+ ```python
76
+ from ctbl_tools.git import git
77
+
78
+ x = git("~/Dev/my_repo")
79
+ ```
80
+ The previous will fail if provided path is not a git repo.
81
+
82
+ Then you may either get the remote urls with `x.get_remote()`, or a get the status of the working tree by invoking `x.get_status()`.
83
+
84
+ You may also do a commit with a list of files with `x.commit(list-of-files)`, or do a push (`x.git_push('my-commit-message')`) or a pull (`x.git_pull()`).
85
+
86
+ This package also includes two auxiliary functions:
87
+
88
+ * `git_clone(url:str, path:str)`: It clones `url` into `path`.
89
+ * `is_git_url(url:str)`: It checks whether `url` is a valid git url.
ctbl-0.1.11/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # ctbl_tools: miscelaneous daily tasks helpers
2
+
3
+ A package with miscelaneous classes meant to help at coding console helpers. Currently there are three classes:
4
+
5
+ 1. **config**: it implements a config file that saves itself. It is a specialization of [configparser](https://docs.python.org/3/library/configparser.html).
6
+ 2. **process**: it runs external processes, it saves the return code, and it helps (a little bit) to parse the standard output (or the standard error).
7
+ 3. **git**: a simple interface for git commands.
8
+
9
+ ## config: a self-preserving config file
10
+
11
+ It implements a config file that saves itself.
12
+
13
+ The idea is: we want to use a config file for an application, which is a simple text file that contains pairs of values, in a similar fashion to an INI file. If the file exists, it is used; if it doesn't, it is created. We use [configparser](https://docs.python.org/3/library/configparser.html) for this.
14
+
15
+ An example:
16
+
17
+ ```python
18
+ from ctbl_tools.config import config
19
+
20
+ cfg = config(initpath = '~/somewhere/blabla.ini')
21
+ cfg.create_section('my_configuration')
22
+ cfg.set('my_configuration', 'x', 1)
23
+ cfg.set('my_configuration', 'y', '2')
24
+ ```
25
+
26
+ When the program above ends, it will save all created configuration to the file ~/somewhere/blabla.ini.
27
+
28
+ ## process: a helper to run external programs
29
+
30
+ It runs an external program, and it stores the return code and both the standard output and standard error.
31
+
32
+ You first create an object of this type:
33
+ ```python
34
+ p = process()
35
+ ```
36
+
37
+ Then run a command:
38
+ ```python
39
+ p.run("whoami")
40
+ ```
41
+
42
+ Immediately after you gain access to the return code, stdout and stderr:
43
+ ```python
44
+ print(p.returncode)
45
+ for line in p.stdout:
46
+ print(line)
47
+ ```
48
+
49
+ You may check if return code was 0 (i.e., all good) by checking `p.is_ok()` to be true. You may also check whether there is any stdout or any stderr with `p.is_there_stdout()` and with `p.is_there_stderr()`, respectively.
50
+
51
+ Finally you may extract information out of stdout by using `extract()`. This method receives a regular expression and a boolean that is true by default (meaning that you want to inspect stdout; if false it will inspect stderr instead). The method will look for all matches in each line of stdout and store them in a list. The list stores as many elements as lines stdout has, and each element is a list of all matches within the corresponding line.
52
+
53
+ ## git: a simple git interface
54
+
55
+ It provides a simple programmatical interface to git.
56
+
57
+ First, you create a git object by providing a local path to a repo:
58
+
59
+ ```python
60
+ from ctbl_tools.git import git
61
+
62
+ x = git("~/Dev/my_repo")
63
+ ```
64
+ The previous will fail if provided path is not a git repo.
65
+
66
+ Then you may either get the remote urls with `x.get_remote()`, or a get the status of the working tree by invoking `x.get_status()`.
67
+
68
+ You may also do a commit with a list of files with `x.commit(list-of-files)`, or do a push (`x.git_push('my-commit-message')`) or a pull (`x.git_pull()`).
69
+
70
+ This package also includes two auxiliary functions:
71
+
72
+ * `git_clone(url:str, path:str)`: It clones `url` into `path`.
73
+ * `is_git_url(url:str)`: It checks whether `url` is a valid git url.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ packages = ["src/tools", "src/cylon"]
7
+
8
+ [project]
9
+ name = "ctbl"
10
+ version = "0.1.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Cristian Bravo Lillo", email = "cristian.bravo@gmail.com" },
14
+ ]
15
+ description = "A set of helpers classes for daily tasks."
16
+ readme = "README.md"
17
+ requires-python = ">=3.8"
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3.8",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Development Status :: 3 - Alpha",
22
+ "Environment :: Console",
23
+ "Operating System :: POSIX :: Linux",
24
+ "Topic :: Utilities",
25
+ ]
@@ -0,0 +1,52 @@
1
+ __all__ = ["cprint", "get_remote_folders"]
2
+
3
+ import re
4
+ import json
5
+ from colorama import Fore, Style
6
+ from tools.process import process
7
+
8
+ #> -----------------------------------------------------------------------------------
9
+ def cprint(arg:str, return_it:bool = False, end:str = "\n"):
10
+ """Prints colored output.
11
+
12
+ This method prints colored output. The argument has to specify which substrings
13
+ should be colored through the following format:
14
+
15
+ "... bla bla bla [r|this text will be in red] and [b|this one will go in blue]..."
16
+
17
+ Allowed colors are r (red), g (green), b (blue) and y (yellow)."""
18
+
19
+ arg = re.sub(r"\[R\|([^]]+?)\]", Fore.RED + Style.BRIGHT + r"\1" + Style.RESET_ALL, arg)
20
+ arg = re.sub(r"\[G\|([^]]+?)\]", Fore.GREEN + Style.BRIGHT + r"\1" + Style.RESET_ALL, arg)
21
+ arg = re.sub(r"\[B\|([^]]+?)\]", Fore.BLUE + Style.BRIGHT + r"\1" + Style.RESET_ALL, arg)
22
+ arg = re.sub(r"\[Y\|([^]]+?)\]", Fore.YELLOW + Style.BRIGHT + r"\1" + Style.RESET_ALL, arg)
23
+
24
+ arg = re.sub(r"\[r\|([^]]+?)\]", Fore.RED + r"\1" + Style.RESET_ALL, arg)
25
+ arg = re.sub(r"\[g\|([^]]+?)\]", Fore.GREEN + r"\1" + Style.RESET_ALL, arg)
26
+ arg = re.sub(r"\[b\|([^]]+?)\]", Fore.BLUE + r"\1" + Style.RESET_ALL, arg)
27
+ arg = re.sub(r"\[y\|([^]]+?)\]", Fore.YELLOW + r"\1" + Style.RESET_ALL, arg)
28
+
29
+ if return_it:
30
+ return arg
31
+ else:
32
+ print(arg, end=end)
33
+ return None
34
+
35
+ #> -----------------------------------------------------------------------------------
36
+ def get_remote_folders(rmt_server:str, rmt_path:str, rmt_file:str = 'cylon.json') -> dict:
37
+ """
38
+ It gets all remote folders as a list, using the cylon convention.
39
+
40
+ If rmt_server or rmt_path are empty, it returns an empty list.
41
+ """
42
+
43
+ if not rmt_server or not rmt_path:
44
+ raise ValueError("rmt_server and rmt_path must be non-empty strings")
45
+
46
+ tst = process()
47
+ tst.run(f"ssh {rmt_server} cat '{rmt_path}/{rmt_file}'")
48
+ if not tst.is_ok():
49
+ raise OSError(f"Cannot run ssh {rmt_server} cat '{rmt_path}/{rmt_file}'")
50
+
51
+ content = "\n".join(tst.stdout)
52
+ return json.loads(content)
@@ -0,0 +1,171 @@
1
+ import sys, getopt
2
+ import os.path
3
+ import cylon
4
+
5
+ class interface:
6
+ """Parsea la línea de comando, recupera los argumentos y opciones, y los guarda para consulta posterior en el diccionario 'model'.
7
+ 'model' tiene dos elementos: 'opts', que es a su vez un diccionario con todas las opciones especificadas por CLI, y 'args', que guarda
8
+ los argumentos especificados. De momento estamos usando solo un argumento.
9
+ """
10
+ model = {}
11
+ verbose = False
12
+
13
+ def __init__(self, options:str = '') -> None:
14
+ """Toma cada uno de los caracteres en 'options' y revisa si recibió alguno como opción en la CLI. Si detecta una opción en la CLI
15
+ que no fue especificada en 'options', termina con un error."""
16
+
17
+ try:
18
+ opts,args = getopt.getopt(sys.argv[1:], options)
19
+
20
+ except getopt.GetoptError as err:
21
+ cylon.cprint(f"[r|{err}]. Giving up.\n")
22
+ exit(1)
23
+
24
+ self.model = {
25
+ 'opts': {},
26
+ 'args': args
27
+ }
28
+
29
+ for i in options:
30
+ self.model['opts'][i] = False
31
+
32
+ for opt,_ in opts:
33
+ opt = opt[1:]
34
+ self.model['opts'][opt] = True
35
+
36
+ if self.is_opt('v'):
37
+ self.verbose = True
38
+
39
+ def assert_command(self, comm:str) -> bool:
40
+ """Chequea si el comando en línea de comando es 'comm'."""
41
+ if self.model['args'] and len(self.model['args'])>0 and self.model['args'][0] == comm:
42
+ if self.verbose:
43
+ cylon.cprint(f"\b\b> {comm}")
44
+ return True
45
+ else:
46
+ return False
47
+
48
+ def is_opt(self, opt:str) -> bool:
49
+ """Chequea si la opción 'opt' fue indicada por CLI."""
50
+ return opt != "" and opt in self.model['opts'] and self.model['opts'][opt]
51
+
52
+ def output(self, msg:str, msg_v:str = ''):
53
+ """Si estamos en modo "simple", imprime 'msg'; si estamos en modo verboso, imprime 'msg_v'."""
54
+ cylon.cprint(msg) if not self.verbose else cylon.cprint(msg_v)
55
+
56
+ def ask_yes_no(self, txt:str, default:str = 'y', quit_if_yes:bool = False, quit_if_no:bool = False) -> str:
57
+ """Presenta el texto 'txt', y pregunta 'sí' (y) o 'no' (n). Usa 'default' para decidir cuál de las dos opciones es la
58
+ default; si el usuario no ingresa nada, se entiende que escoge la opción default."""
59
+ print(txt + " ", end='')
60
+
61
+ match default:
62
+ case 'y':
63
+ qst = "Y/n"
64
+ case 'n':
65
+ qst = "y/N"
66
+ case _:
67
+ qst = "y/n"
68
+
69
+ res = input(f"({qst}): ")
70
+
71
+ if res == '' and default in ['y','n']:
72
+ res = default
73
+
74
+ if (quit_if_yes and res=='y') or (quit_if_no and res=='n'):
75
+ print("OK, no problem")
76
+ exit(0)
77
+
78
+ return res
79
+
80
+ def ask_for_options(self, options:dict[str,str], question:str) -> str:
81
+ """Presenta la lista 'options' de forma numerada, luego presenta el texto 'question', y pide ingresar un número para las opciones.
82
+ El número 0 se usa para salir del menú."""
83
+ i=0
84
+
85
+ cylon.cprint("\t[b|quit> Quit!]\n")
86
+ for tmp in options:
87
+ cylon.cprint(f"\t[b|{tmp}> {options[tmp]}]\n")
88
+
89
+ key = ''
90
+ while key == '' or key not in options:
91
+ key = input(question + ' ')
92
+
93
+ if key == 'quit':
94
+ exit(0)
95
+
96
+ return key
97
+
98
+ def pick_remote(self, lcng):
99
+ if self.verbose:
100
+ self.ask_yes_no(f"Do I get options from {lcng.get_remote_url()}?", quit_if_no=True)
101
+ else:
102
+ self.ask_yes_no(f"Check {lcng.get_remote_url()}?", quit_if_no=True)
103
+
104
+ rmt = self.get_remote_folders(lcng)
105
+
106
+ # Cuantos remote folders tenemos?
107
+ match len(rmt):
108
+ # Si no hay ninguno, estamos jodidos
109
+ case 0:
110
+ cylon.cprint("[r|No cylons available]. Giving up.\n")
111
+ exit(0)
112
+
113
+ # Si hay un solo folder remoto, lo guardamos para preguntar si lo bajamos o no.
114
+ case 1:
115
+ self.ask_yes_no(f"Do I download {rmt[0]}?", quit_if_no=True)
116
+ whichone = rmt[0]
117
+
118
+ # Si hay más de uno, mostramos los que hay y preguntamos cuál se quiere bajar.
119
+ case _:
120
+ opt = self.ask_for_options(rmt, "Which one do I download?")
121
+ whichone = rmt[opt]
122
+ return whichone
123
+
124
+ def get_remote_folders(self, lcng):
125
+ out = False
126
+ rmt = {}
127
+
128
+ while not out:
129
+ rmt = cylon.get_remote_folders(lcng.get('remote_server'), lcng.get('remote_path'))
130
+ if not rmt:
131
+ self.ask_yes_no("No answer. Should I try again?", quit_if_no=True)
132
+ else:
133
+ out = True
134
+
135
+ return rmt
136
+
137
+ def pick_target(self, lcng, whichone):
138
+ # Preguntamos dónde tenemos que bajar el folder remoto
139
+ ok = False
140
+ target = ''
141
+
142
+ while not ok:
143
+ if self.verbose:
144
+ target = input(f"Where should I download {whichone} to? (~/lib by default): ")
145
+ else:
146
+ target = input("Where to? (~/lib by default): ")
147
+
148
+ if target == '':
149
+ target = '~/lib'
150
+
151
+ if os.path.exists(target):
152
+ self.output(
153
+ f"{target} exists. Pick another.\n",
154
+ f"[r|{target} already exists]. I refuse to overwrite it. Pick another folder.\n"
155
+ )
156
+ else:
157
+ ok = True
158
+ return target
159
+
160
+ def confirm_download(self, whichone, target):
161
+ self.output(
162
+ f"[y|{whichone}] -> [y|{target}]. ",
163
+ f"I'll download [b|{whichone}] to [b|{target}]. "
164
+ )
165
+ self.ask_yes_no("Is that OK?", quit_if_no=True)
166
+
167
+ def confirm_patching(self, folder):
168
+ if self.verbose:
169
+ self.ask_yes_no(f"Do I point the env var to {folder}?", default='y', quit_if_no=True)
170
+ else:
171
+ self.ask_yes_no(f"env var -> {folder}?", default='y', quit_if_no=True)
@@ -0,0 +1,141 @@
1
+ """It implements a copy of a cylon: a git package, retrieved from a source.
2
+
3
+ A skin is a simply a set of folders and files to be put at the root of a user account in a server, with a set
4
+ of instructions about what to do to deploy. For example, I would like to copy most files, and then create symbolic
5
+ links to these files. Sometimes I would like to create certain files out of some content. All of these instructions
6
+ are contained within a config file, that is implemented through cbl_tools/config.
7
+
8
+ This class has no CLI interaction with the user.
9
+ """
10
+
11
+ import os.path
12
+ from datetime import datetime
13
+ from tools import norm_path
14
+ from tools.config import config
15
+ from tools.process import process
16
+ from tools.git import git
17
+
18
+ class skin(config):
19
+
20
+ def __init__(self, path:str, touch_inifile:bool = False) -> None:
21
+ """It loads a skin located in path. Path should be the root of the cylon folder,
22
+ not the files/ folder which contains the ini file.
23
+
24
+ Loading a skin means to load a local folder, already downloaded from a source, and
25
+ reading the config file in order to check whether this copy is in good condition.
26
+ """
27
+ super().__init__(initpath=os.path.join(path, 'files/cylon.ini'), create_folder=False)
28
+
29
+ # Seteamos valores importantes en cylon.ini
30
+ self.set('local', 'home', os.getenv('HOME'))
31
+ self.set('local', 'sh', os.getenv('SHELL'))
32
+ self._set_value('uname -o', 'os')
33
+ self._set_value('uname -n', 'node')
34
+ self._set_value('uname -p', 'arch')
35
+
36
+ # Crea un objeto git y lo asocia a una variable interna
37
+ self.gitobj = git(self.get_dirname())
38
+ self.set('local', 'path', self.gitobj.local_path)
39
+
40
+ # Finalmente, registra un método que actualiza el valor de "last_modified"
41
+ if touch_inifile:
42
+ self.register(self._set_postvalue)
43
+
44
+ def _set_value(self, com:str, label:str) -> None:
45
+ p = process()
46
+ p.run(com)
47
+ if not p.is_ok() or not p.is_there_stdout():
48
+ raise OSError(f"Cannot run {com}")
49
+ self.set('local', label, p.stdout[0])
50
+
51
+ def _set_postvalue(self) -> None:
52
+ self.set('local', 'last_modified', datetime.now().strftime('%Y%m%d%H%M%S'))
53
+
54
+ def fix_links(self, color:bool = True) -> list:
55
+ """Checks all links in section "symlinks", and fixes them if they are not currently set.
56
+
57
+ The idea is: the section "symlinks" contains a set of pairs. The key is a symlink in the root of the user ("a link"),
58
+ and the value is a file within the "path" of the cylon folder ("the target"). This method checks whether the target
59
+ exists (if not, it complains), whether the link exists and is not a link (if not, it complains), and whether the link
60
+ indeed points to the target (if not, it corrects the link so it points to the target).
61
+ """
62
+
63
+ results = []
64
+ for key in self.options("symlinks"):
65
+ link = os.path.join(self.get("local", "home"), key)
66
+ target = norm_path(self.get('local', 'path'), self.get("symlinks", key))
67
+ result = {'link': link, 'target': target, 'OK':False}
68
+
69
+ if not os.path.exists(target):
70
+ result['OK'] = False
71
+ if color:
72
+ result['msg'] = f"[r|Target {target} in config file does not exist]. Please check."
73
+ else:
74
+ result['msg'] = f"Target {target} in config file does not exist. Please check."
75
+
76
+ elif os.path.exists(link) and not os.path.islink(link):
77
+ result['OK'] = False
78
+ if color:
79
+ result['msg'] = f"[r|Link {link} mentioned in config file exists, and is not a link]. Please check."
80
+ else:
81
+ result['msg'] = f"Link {link} mentioned in config file exists, and is not a link. Please check."
82
+
83
+ elif os.path.lexists(link) and os.path.realpath(link) != target:
84
+ os.remove(link)
85
+ result['OK'] = True
86
+ if color:
87
+ result['msg'] = f"[r|{link} points to {os.path.realpath(link)} instead of {target}], so fixing it..."
88
+ else:
89
+ result['msg'] = f"{link} points to {os.path.realpath(link)} instead of {target}, so fixing it..."
90
+
91
+ elif not os.path.exists(link):
92
+ os.symlink(target, link)
93
+ result['OK'] = True
94
+ if color:
95
+ result['msg'] = f"[y|Creating {link} -> {target}]"
96
+ else:
97
+ result['msg'] = f"Creating {link} -> {target}"
98
+ else:
99
+ result['OK'] = None
100
+ result['msg'] = "No change"
101
+
102
+ results.append(result)
103
+ return results
104
+
105
+ def fix_xdg_dirs(self) -> dict[str, list[str]]:
106
+ """Checks all user dirs, based on the xdg group. Returns a dict with two arrays: one for all stuff that went
107
+ well, and another for all that didn't.
108
+
109
+ The idea is: the group "xdg" contains the user dirs, as defined by the unix manual "man xdg-user-dirs-update".
110
+ This method cycles through all keys in that section, and points them to the correct user dir. If the special
111
+ keyword "None" is used, that means that we don't want to have a dir for that, so that key is set to /dev/null.
112
+ """
113
+
114
+ p = process()
115
+ results = {
116
+ "Yes": [],
117
+ "No": []
118
+ }
119
+
120
+ for key in self.options("xdg"):
121
+
122
+ if self.get("xdg", key) == "None":
123
+ p.run(f"xdg-user-dirs-update --set {key} /dev/null")
124
+ if p.is_ok():
125
+ results['Yes'].append(f"xdg:{key} set to None")
126
+ else:
127
+ results['No'].append(f"xdg:{key} does not exist!")
128
+ continue
129
+
130
+ val = os.path.join(self.get("local", "path"), self.get("xdg", key))
131
+
132
+ if not os.path.exists(val):
133
+ results['No'].append(f"xdg:{key} points to non-existent file/folder!")
134
+ else:
135
+ p.run(f"xdg-user-dirs-update --set {key} {val}")
136
+ if p.is_ok():
137
+ results['Yes'].append(f"xdg:{key} set to {val}")
138
+ else:
139
+ results['No'].append(f"xdg:{key} does not exist!")
140
+
141
+ return results
@@ -0,0 +1,32 @@
1
+ __all__ = ["create_tempdir", "norm_path", "is_file_readable"]
2
+
3
+ import os.path
4
+ import random
5
+ import tempfile
6
+
7
+ #> -----------------------------------------------------------------------------------
8
+ def create_tempdir(mkdir:bool = False) -> str:
9
+ random.seed()
10
+
11
+ p = tempfile.gettempdir()
12
+ while True:
13
+ fld = "cbl-config-" + str(random.randint(100000, 999999))
14
+ thispath = os.path.join(p, fld)
15
+ if not os.path.exists(thispath):
16
+ break
17
+
18
+ if mkdir:
19
+ os.mkdir(thispath)
20
+
21
+ return thispath
22
+
23
+ #> -----------------------------------------------------------------------------------
24
+ def norm_path(*args) -> str:
25
+ if len(args) == 0 or not args[0]:
26
+ return ""
27
+ else:
28
+ return os.path.normpath(os.path.expanduser(os.path.join(*args)))
29
+
30
+ #> -----------------------------------------------------------------------------------
31
+ def is_file_readable(file:str) -> bool:
32
+ return file != "" and os.path.exists(file) and os.path.isfile(file) and os.access(file, os.R_OK)