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 +11 -0
- ctbl-0.1.11/LICENSE +21 -0
- ctbl-0.1.11/PKG-INFO +89 -0
- ctbl-0.1.11/README.md +73 -0
- ctbl-0.1.11/pyproject.toml +25 -0
- ctbl-0.1.11/src/cylon/__init__.py +52 -0
- ctbl-0.1.11/src/cylon/interface.py +171 -0
- ctbl-0.1.11/src/cylon/skin.py +141 -0
- ctbl-0.1.11/src/tools/__init__.py +32 -0
- ctbl-0.1.11/src/tools/config.py +101 -0
- ctbl-0.1.11/src/tools/exceptions.py +12 -0
- ctbl-0.1.11/src/tools/git.py +222 -0
- ctbl-0.1.11/src/tools/prob.py +36 -0
- ctbl-0.1.11/src/tools/process.py +85 -0
- ctbl-0.1.11/tests/config_tests.py +91 -0
- ctbl-0.1.11/tests/cylon.ini +81 -0
- ctbl-0.1.11/tests/general_tests.py +22 -0
- ctbl-0.1.11/tests/git_tests.py +48 -0
- ctbl-0.1.11/tests/process_tests.py +45 -0
- ctbl-0.1.11/tests/skin_tests.py +65 -0
ctbl-0.1.11/.gitignore
ADDED
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)
|