ctbl 0.1.11__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.
- ctbl-0.1.11.dist-info/METADATA +89 -0
- ctbl-0.1.11.dist-info/RECORD +13 -0
- ctbl-0.1.11.dist-info/WHEEL +4 -0
- ctbl-0.1.11.dist-info/licenses/LICENSE +21 -0
- cylon/__init__.py +52 -0
- cylon/interface.py +171 -0
- cylon/skin.py +141 -0
- tools/__init__.py +32 -0
- tools/config.py +101 -0
- tools/exceptions.py +12 -0
- tools/git.py +222 -0
- tools/prob.py +36 -0
- tools/process.py +85 -0
|
@@ -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.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
cylon/__init__.py,sha256=y53Q0TRluziSSKygMFzCCMsbOjlLsrQxiE-JM9j4kLg,2122
|
|
2
|
+
cylon/interface.py,sha256=tRVOIEzOoDm_5d-lWd7V1dLip5kvJ3RatydRKTDBWVQ,5993
|
|
3
|
+
cylon/skin.py,sha256=qRAp3Xa1W--RGFDlEoUvP7EOhH25saULqoW2R6qfbTc,6339
|
|
4
|
+
tools/__init__.py,sha256=jk6C-Ht-VrJ8DU_9slDWEQSuDHsWhNG_W-HawB37r7A,1022
|
|
5
|
+
tools/config.py,sha256=VnCGvIX8whWAv1Mybsh-0jcuNa0TS6VBo2y7XfSQFS4,4262
|
|
6
|
+
tools/exceptions.py,sha256=aI0JXEHrPyScHD5d73XtXFK7QD4mp29QZhuY-Qe8SZI,178
|
|
7
|
+
tools/git.py,sha256=Jz6mnWPbeDaWvtJ6RHlstIfAPwP1lSlT100nwP8n3hQ,7466
|
|
8
|
+
tools/prob.py,sha256=VopW8sM5kthyhQ6s1V0U3B-BDBDFDhw4bQ3SZ1maFFU,991
|
|
9
|
+
tools/process.py,sha256=xr2vYu3wGvZKWuHVQD4urTNNAhn7lAKt1RAKZIBD6lY,2797
|
|
10
|
+
ctbl-0.1.11.dist-info/METADATA,sha256=sXb0QGqXYrhB3aMQdYq0qOfaWoGwnxkaEPj5fGZvLYY,3636
|
|
11
|
+
ctbl-0.1.11.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
12
|
+
ctbl-0.1.11.dist-info/licenses/LICENSE,sha256=v9QcLF_Lav1RCnJPMKZB_b7wQ6AGgiRwg5SdlvhfTMk,1106
|
|
13
|
+
ctbl-0.1.11.dist-info/RECORD,,
|
|
@@ -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.
|
cylon/__init__.py
ADDED
|
@@ -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)
|
cylon/interface.py
ADDED
|
@@ -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)
|
cylon/skin.py
ADDED
|
@@ -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
|
tools/__init__.py
ADDED
|
@@ -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)
|
tools/config.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""It implements a config file that saves itself.
|
|
2
|
+
|
|
3
|
+
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
|
|
4
|
+
exists, it is used; if it doesn't, it is created. We use configparser for this.
|
|
5
|
+
|
|
6
|
+
This class has just one public variable: path, which is the absolute path formed by initpath and filename, arguments to the constructor of the class.
|
|
7
|
+
|
|
8
|
+
_values is the configparser where one will find all the values that were read, if present. I'm not sure if I should make it public though.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import atexit
|
|
13
|
+
import errno
|
|
14
|
+
import configparser
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
from configparser import ExtendedInterpolation
|
|
17
|
+
from tools.exceptions import *
|
|
18
|
+
|
|
19
|
+
class config(configparser.ConfigParser):
|
|
20
|
+
|
|
21
|
+
path:str = ""
|
|
22
|
+
|
|
23
|
+
def __init__(self, initpath:str = '~/.config/config.ini', create_folder:bool = True, default_section:str = 'default') -> None:
|
|
24
|
+
"""It creates a config file.
|
|
25
|
+
|
|
26
|
+
To create the file, we specify its path (by default, '~/.config/config.ini'). The last folder within the path may not exist,
|
|
27
|
+
in which case it will be created. Neither initpath nor filename could be empty (an OSError will be raised). We can also specify whether an inexistent folder
|
|
28
|
+
should be created with create_folder (by default, True).
|
|
29
|
+
|
|
30
|
+
If thispath is empty it raises an error. If the last portion of thispath doesn't exist and create_folder is False, it raises a FileNotFoundError
|
|
31
|
+
since we're being asked to use a folder that doesn't exist, and we're being told not to create it.
|
|
32
|
+
|
|
33
|
+
Finally, if a file exists in initpath, it's read and put into self.values. If it doesn't exist, it will be created when the
|
|
34
|
+
program is terminated, or when flush() is invoked.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if not initpath:
|
|
38
|
+
raise EmptyValueError("initpath must not be empty")
|
|
39
|
+
|
|
40
|
+
folder,filename = os.path.split(os.path.normpath(os.path.expanduser(initpath)))
|
|
41
|
+
|
|
42
|
+
if not os.path.exists(folder) and not create_folder:
|
|
43
|
+
raise FileNotFoundError(errno.ENOENT, "path does not exist and create_folder is False", folder)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
if not os.path.exists(folder) and create_folder:
|
|
47
|
+
os.mkdir(folder, 0o700)
|
|
48
|
+
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
raise FileNotFoundError(errno.ENOENT, "one or more of the parent folders in thispath don't exist, and won't be created", folder)
|
|
51
|
+
|
|
52
|
+
except PermissionError:
|
|
53
|
+
raise PermissionError(errno.EACCES, "cannot create folder: permission denied", folder)
|
|
54
|
+
|
|
55
|
+
self.path = os.path.join(folder, filename)
|
|
56
|
+
|
|
57
|
+
if os.path.exists(self.path) and not os.path.isfile(self.path):
|
|
58
|
+
raise NotAFileError("last part of initpath already exists, and it's not a file")
|
|
59
|
+
|
|
60
|
+
# If path goes out of normalized init_path, the user may be abusing this class, so we ban it
|
|
61
|
+
if os.path.commonpath([self.path, initpath]) != initpath:
|
|
62
|
+
raise ValueError(f"filename ({filename}) is relative to initpath ({initpath}), and it should be within it, but it's not ({self.path})")
|
|
63
|
+
|
|
64
|
+
super().__init__(delimiters=('='), comment_prefixes=('#'), interpolation=ExtendedInterpolation(), default_section=default_section)
|
|
65
|
+
# self.optionxform = str
|
|
66
|
+
|
|
67
|
+
# Si el archivo existe, hay que leerlo
|
|
68
|
+
if os.path.exists(self.path):
|
|
69
|
+
super().read(self.path)
|
|
70
|
+
|
|
71
|
+
self.register(self._flush)
|
|
72
|
+
|
|
73
|
+
def __str__(self) -> str:
|
|
74
|
+
return f"<{__class__.__name__} object; path={self.path}>"
|
|
75
|
+
|
|
76
|
+
def _flush(self) -> None:
|
|
77
|
+
f = open(self.path, "w")
|
|
78
|
+
self.write(f)
|
|
79
|
+
f.close()
|
|
80
|
+
|
|
81
|
+
def get_dirname(self) -> str:
|
|
82
|
+
if self.path:
|
|
83
|
+
return os.path.dirname(self.path)
|
|
84
|
+
else:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
def get_filename(self) -> str:
|
|
88
|
+
if self.path:
|
|
89
|
+
return os.path.basename(self.path)
|
|
90
|
+
else:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
def register(self, func:Callable) -> None:
|
|
94
|
+
atexit.register(func)
|
|
95
|
+
|
|
96
|
+
def section(self, section:str) -> dict:
|
|
97
|
+
"It returns all the values in a section, but as a dict instead of a list of tuples."
|
|
98
|
+
sct = {}
|
|
99
|
+
for x,y in self.items(section):
|
|
100
|
+
sct[x] = y
|
|
101
|
+
return sct
|
tools/exceptions.py
ADDED
tools/git.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""It provides a simple programmatical interface to git.
|
|
2
|
+
|
|
3
|
+
First, you create a git object by providing a local path to a repo:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from cbl_tools.git import git
|
|
7
|
+
|
|
8
|
+
x = git("~/Dev/my_repo")
|
|
9
|
+
```
|
|
10
|
+
The previous will fail if provided path is not a git repo.
|
|
11
|
+
|
|
12
|
+
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()`.
|
|
13
|
+
|
|
14
|
+
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()`).
|
|
15
|
+
|
|
16
|
+
This package also includes two auxiliary functions:
|
|
17
|
+
|
|
18
|
+
* `git_clone(url:str, path:str)`: It clones `url` into `path`.
|
|
19
|
+
* `is_git_url(url:str)`: It checks whether `url` is a valid git url.
|
|
20
|
+
* `is_git_folder(path:str, return_folder:bool = False)`: It checks whether `path` is a cylon folder or one of its subfolders. If `return_folder` is False,
|
|
21
|
+
it returns a boolean with the result. Otherwise, it returns the path of the root of the cylon folder.
|
|
22
|
+
"""
|
|
23
|
+
import re
|
|
24
|
+
import errno
|
|
25
|
+
import os.path
|
|
26
|
+
from tools import process
|
|
27
|
+
from tools import norm_path
|
|
28
|
+
from tools.exceptions import EmptyValueError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def git_clone(url:str, path:str) -> bool:
|
|
32
|
+
"""It clones url to the local path."""
|
|
33
|
+
|
|
34
|
+
path = norm_path(path)
|
|
35
|
+
if os.path.exists(path):
|
|
36
|
+
raise FileExistsError(f"{path} already exists")
|
|
37
|
+
|
|
38
|
+
if not is_git_url(url):
|
|
39
|
+
raise ValueError(f"{url} is not a valid git url")
|
|
40
|
+
|
|
41
|
+
p = process.process()
|
|
42
|
+
p.run(f"git clone {url} {path}")
|
|
43
|
+
return p.is_ok()
|
|
44
|
+
|
|
45
|
+
def is_git_url(url:str)-> bool:
|
|
46
|
+
"""It tests url to be one of the five valid url-like git strings."""
|
|
47
|
+
if not url:
|
|
48
|
+
raise EmptyValueError("empty url")
|
|
49
|
+
|
|
50
|
+
allw = r"[\w\-\d\.]"
|
|
51
|
+
user = fr"({allw}+@)"
|
|
52
|
+
host = fr"{allw}+(\.{allw}+)+"
|
|
53
|
+
port = r"(:\d+)"
|
|
54
|
+
path = fr"(/{allw}+)*"
|
|
55
|
+
|
|
56
|
+
# Type of URL #1: via SSH
|
|
57
|
+
if re.fullmatch(fr"ssh://{user}?{host}{port}?{path}", url):
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
# Type of URL #2: via HTTP(S), FTP(S), or via a pseudo-url with GIT as protocol
|
|
61
|
+
if re.fullmatch(fr"((ht|f)tps?|git)://{user}?{host}{port}?{path}", url):
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
# Type of URL #3: via user and host, which is for a private server
|
|
65
|
+
if re.fullmatch(fr"{user}?{host}:~?{allw}+{path}", url):
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def is_git_folder(path:str) -> bool:
|
|
71
|
+
|
|
72
|
+
# We run a rev-parse
|
|
73
|
+
path = norm_path(path)
|
|
74
|
+
p = process.process()
|
|
75
|
+
p.run(f"git -C {path} rev-parse --absolute-git-dir")
|
|
76
|
+
|
|
77
|
+
# If there was something wrong, it's not a git folder
|
|
78
|
+
if not p.is_ok() or p.is_there_stderr() or not p.is_there_stdout():
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
# We split the returned line and check if there is a dir there
|
|
82
|
+
is_git = os.path.isdir(os.path.dirname(p.stdout[0])) and os.path.basename(p.stdout[0]) == '.git'
|
|
83
|
+
|
|
84
|
+
# if return_folder and is_git:
|
|
85
|
+
# return os.path.dirname(p.stdout[0])
|
|
86
|
+
|
|
87
|
+
# if return_folder:
|
|
88
|
+
# return None
|
|
89
|
+
|
|
90
|
+
return is_git
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class git:
|
|
94
|
+
|
|
95
|
+
local_path:str = ""
|
|
96
|
+
remote_path:dict = {}
|
|
97
|
+
|
|
98
|
+
def __init__(self, path:str) -> None:
|
|
99
|
+
"""This method takes a local path and treats it as if it were a cylon repo;
|
|
100
|
+
that is, it expects to find a git repo, with a files/ folder and a cylon.
|
|
101
|
+
"""
|
|
102
|
+
# If normed path is not a folder, we reject it
|
|
103
|
+
path = norm_path(path)
|
|
104
|
+
if not os.path.isdir(path):
|
|
105
|
+
raise NotADirectoryError(errno.ENOTDIR, f"{path} is not a folder", path)
|
|
106
|
+
|
|
107
|
+
# We ask if it's a git folder
|
|
108
|
+
if not is_git_folder(path):
|
|
109
|
+
raise ValueError("Argument is not a git folder")
|
|
110
|
+
|
|
111
|
+
p = process.process()
|
|
112
|
+
p.run(f"git -C {path} rev-parse --absolute-git-dir")
|
|
113
|
+
|
|
114
|
+
if not p.is_ok() or not p.is_there_stdout() or p.is_there_stderr():
|
|
115
|
+
raise ValueError("Internal inconsistency in the arguments")
|
|
116
|
+
|
|
117
|
+
self.local_path = os.path.dirname(p.stdout[0])
|
|
118
|
+
|
|
119
|
+
def get_remote(self) -> dict:
|
|
120
|
+
"""It retrieves the list of remote repositories (usually just one), with their 'fetch' and 'push'
|
|
121
|
+
URLs (usually the same). It returns (and stores within self.remote_path) a dictionary with the
|
|
122
|
+
following structure:
|
|
123
|
+
|
|
124
|
+
name-of-repo -> {
|
|
125
|
+
'fetch' -> ['username', 'server', 'path'],
|
|
126
|
+
'push' -> ['username', 'server', 'path']
|
|
127
|
+
}
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
p = process.process()
|
|
131
|
+
p.run(f"git -C {self.local_path} remote -v")
|
|
132
|
+
|
|
133
|
+
if not p.is_ok() or p.is_there_stderr() or not p.is_there_stdout():
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
res = {}
|
|
137
|
+
for line in p.stdout:
|
|
138
|
+
x = re.fullmatch(r"(\w+)\t(\S+)\s\((fetch|push)\)", line)
|
|
139
|
+
if x:
|
|
140
|
+
if not x.group(1) in res:
|
|
141
|
+
res[x.group(1)] = {}
|
|
142
|
+
res[x.group(1)][x.group(3)] = self._url_split(x.group(2))
|
|
143
|
+
self.remote_path = res
|
|
144
|
+
return res
|
|
145
|
+
|
|
146
|
+
def _url_split(self, arg:str) -> list:
|
|
147
|
+
tst = re.search(r"([^@]+?)@([^:]+?):(.+)", arg)
|
|
148
|
+
|
|
149
|
+
if tst:
|
|
150
|
+
url_user = tst.group(1)
|
|
151
|
+
url_domain = tst.group(2)
|
|
152
|
+
url_path = tst.group(3)
|
|
153
|
+
|
|
154
|
+
if url_path.startswith("~" + url_user):
|
|
155
|
+
url_path = '~' + url_path[len(url_user)+1:]
|
|
156
|
+
return [url_user, url_domain, url_path]
|
|
157
|
+
else:
|
|
158
|
+
return [None, None, arg]
|
|
159
|
+
|
|
160
|
+
def get_url(self, arg:str = "") -> str:
|
|
161
|
+
"""If self.remote_path has been populated, it returns the remote url for the only repo;
|
|
162
|
+
otherwise, it returns an empty string. If there is more than one repo, you should provide its name
|
|
163
|
+
as argument; otherwise, it returns an empty string."""
|
|
164
|
+
if not self.remote_path:
|
|
165
|
+
return ""
|
|
166
|
+
|
|
167
|
+
if len(self.remote_path)>1:
|
|
168
|
+
if arg == '':
|
|
169
|
+
return ""
|
|
170
|
+
ln = self.remote_path[arg]
|
|
171
|
+
else:
|
|
172
|
+
ln = self.remote_path[list(self.remote_path.keys())[0]]
|
|
173
|
+
|
|
174
|
+
return ln[0] + '@' + ln[1] + ':' + ln[2]
|
|
175
|
+
|
|
176
|
+
def get_status(self) -> dict:
|
|
177
|
+
"""It gets the status of the working tree in the porcelain v1 format (described
|
|
178
|
+
in the git manual for status). It returns a dictionary (possibly empty). The keys
|
|
179
|
+
of the dict are the XY codes described for the porcelain v1 format, while the
|
|
180
|
+
elements are simple lists with paths of the corresponding files."""
|
|
181
|
+
p = process.process()
|
|
182
|
+
p.run(f"git -C {self.local_path} status --porcelain")
|
|
183
|
+
if not p.is_ok() or not p.is_there_stdout():
|
|
184
|
+
return {}
|
|
185
|
+
|
|
186
|
+
res = {}
|
|
187
|
+
for line in p.stdout:
|
|
188
|
+
x = re.fullmatch(r"(.{2})\s(.+)", line)
|
|
189
|
+
if x:
|
|
190
|
+
if not x.group(1) in res:
|
|
191
|
+
res[x.group(1)] = []
|
|
192
|
+
res[x.group(1)].append(x.group(2))
|
|
193
|
+
|
|
194
|
+
return res
|
|
195
|
+
|
|
196
|
+
def git_add(self, include_new:bool = False) -> bool:
|
|
197
|
+
if include_new:
|
|
198
|
+
opt = '-A'
|
|
199
|
+
else:
|
|
200
|
+
opt = '-u'
|
|
201
|
+
|
|
202
|
+
p = process.process()
|
|
203
|
+
p.run(f"git -C {self.local_path} add {opt}")
|
|
204
|
+
return p.is_ok()
|
|
205
|
+
|
|
206
|
+
def git_commit(self, msg:str = '') -> process.process:
|
|
207
|
+
if not msg:
|
|
208
|
+
msg = 'Automatic commit by cbl_tools.git.commit()'
|
|
209
|
+
|
|
210
|
+
p = process.process()
|
|
211
|
+
p.run(f"git -C {self.local_path} commit -m '{msg}'")
|
|
212
|
+
return p
|
|
213
|
+
|
|
214
|
+
def git_push(self) -> process.process:
|
|
215
|
+
p = process.process()
|
|
216
|
+
p.run(f"git -C {self.local_path} push --all")
|
|
217
|
+
return p
|
|
218
|
+
|
|
219
|
+
def git_pull(self) -> process.process:
|
|
220
|
+
p = process.process()
|
|
221
|
+
p.run(f"git -C {self.local_path} pull")
|
|
222
|
+
return p
|
tools/prob.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from itertools import combinations
|
|
2
|
+
|
|
3
|
+
def p_any(lst, vrb = False):
|
|
4
|
+
|
|
5
|
+
if vrb:
|
|
6
|
+
print("Recibido:", lst)
|
|
7
|
+
|
|
8
|
+
sm = sum(lst)
|
|
9
|
+
|
|
10
|
+
if vrb:
|
|
11
|
+
for i in range(0, len(lst)):
|
|
12
|
+
print(lst[i], " + ", sep="", end="")
|
|
13
|
+
print("")
|
|
14
|
+
|
|
15
|
+
sk = 1
|
|
16
|
+
for x in range(len(lst), 1, -1):
|
|
17
|
+
tuplet = list(combinations(lst, x))
|
|
18
|
+
sk *= -1
|
|
19
|
+
for y in tuplet:
|
|
20
|
+
mlt = 1
|
|
21
|
+
if vrb:
|
|
22
|
+
print("\t+", sk, end="")
|
|
23
|
+
for z in range(0, len(y)):
|
|
24
|
+
mlt *= y[z]
|
|
25
|
+
if vrb:
|
|
26
|
+
print(" * ", y[z], sep="", end="")
|
|
27
|
+
sm += sk * mlt
|
|
28
|
+
if vrb:
|
|
29
|
+
print("")
|
|
30
|
+
|
|
31
|
+
return sm
|
|
32
|
+
|
|
33
|
+
drupal = [.9757, .9754, .9746, .9658, .9286, .9271, .9237, .6823, .4359, .1694, 0.0678, 0.0612, 0.0557, 0.0397, 0.0363, 0.0275, 0.019, 0.0142]
|
|
34
|
+
wordpress = [.0033, .0025, .0016, .0016, .0016, .0014, .0012, .0009, .0008, .0005, .0004]
|
|
35
|
+
wagtail = [.0011, .0009]
|
|
36
|
+
print("\n", p_any(wordpress, True))
|
tools/process.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""It runs an external program, and it stores the return code and both the standard
|
|
2
|
+
output and standard error.
|
|
3
|
+
|
|
4
|
+
You first create an object of this type:
|
|
5
|
+
> p = process()
|
|
6
|
+
|
|
7
|
+
Then run a command:
|
|
8
|
+
> p.run("whoami")
|
|
9
|
+
|
|
10
|
+
Immediately after you gain access to the return code, stdout and stderr:
|
|
11
|
+
> print(p.returncode)
|
|
12
|
+
> for line in p.stdout:
|
|
13
|
+
> print(line)
|
|
14
|
+
|
|
15
|
+
You may check if return code was 0 (i.e., all good) by checking p.is_ok() to be true.
|
|
16
|
+
You may also check whether there is any stdout or any stderr with p.is_there_stdout()
|
|
17
|
+
and with p.is_there_stderr(), respectively.
|
|
18
|
+
|
|
19
|
+
Finally you may extract information out of stdout by using extract(). This method receives
|
|
20
|
+
a regular expression and boolean that is true by default (meaning that you want to inspect
|
|
21
|
+
stdout; if false it will inspect stderr instead). The method will look for all matches in
|
|
22
|
+
each line of stdout and store them in a list. The list stores as many elements as lines
|
|
23
|
+
stdout has, and each element is a list of all matches within the corresponding line.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
import subprocess
|
|
28
|
+
|
|
29
|
+
class process:
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.reset()
|
|
33
|
+
|
|
34
|
+
def reset(self) -> None:
|
|
35
|
+
self.command = ""
|
|
36
|
+
self.returncode = -1
|
|
37
|
+
self.stdout = []
|
|
38
|
+
self.stderr = []
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
if not self.command:
|
|
42
|
+
return "<Empty tools.process object>"
|
|
43
|
+
else:
|
|
44
|
+
out = f"({self.command})->{self.returncode}\n"
|
|
45
|
+
if self.stdout:
|
|
46
|
+
out += ''.join(map(lambda x: 'O: '+x+'\n', self.stdout))
|
|
47
|
+
if self.stderr:
|
|
48
|
+
out += ''.join(map(lambda x: 'E: '+x+'\n', self.stderr))
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
def run(self, comm:str, fail_if_not_ok:bool = False) -> None:
|
|
52
|
+
cp = subprocess.run(comm, shell=True, capture_output=True, text=True)
|
|
53
|
+
self.command = comm
|
|
54
|
+
self.returncode = cp.returncode
|
|
55
|
+
if cp.stdout != '':
|
|
56
|
+
self.stdout = cp.stdout.rstrip(" \n").split("\n")
|
|
57
|
+
if cp.stderr != '':
|
|
58
|
+
self.stderr = cp.stderr.rstrip(" \n").split("\n")
|
|
59
|
+
if fail_if_not_ok and not self.is_ok():
|
|
60
|
+
print(self)
|
|
61
|
+
exit(1)
|
|
62
|
+
|
|
63
|
+
def is_ok(self) -> bool:
|
|
64
|
+
return self.returncode == 0
|
|
65
|
+
|
|
66
|
+
def is_there_stdout(self) -> bool:
|
|
67
|
+
return len(self.stdout)>0
|
|
68
|
+
|
|
69
|
+
def is_there_stderr(self) -> bool:
|
|
70
|
+
return len(self.stderr)>0
|
|
71
|
+
|
|
72
|
+
def extract(self, pat:str, stdout:bool = True, join:bool = True) -> list:
|
|
73
|
+
wheretolook = self.stdout if stdout else self.stderr
|
|
74
|
+
if not wheretolook:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
pattern = re.compile(pat)
|
|
78
|
+
|
|
79
|
+
if join:
|
|
80
|
+
return pattern.findall("".join(wheretolook))
|
|
81
|
+
else:
|
|
82
|
+
lst = []
|
|
83
|
+
for line in wheretolook:
|
|
84
|
+
lst.append(pattern.findall(line))
|
|
85
|
+
return lst
|