mantis-cli 19.2.0__tar.gz → 20.0.0__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.
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/PKG-INFO +4 -7
- mantis_cli-20.0.0/mantis/__init__.py +1 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/command_line.py +96 -54
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/environment.py +31 -3
- mantis_cli-20.0.0/mantis/helpers.py +98 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/logic.py +33 -14
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/managers.py +158 -15
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/mantis.tpl +1 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis_cli.egg-info/PKG-INFO +3 -6
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis_cli.egg-info/entry_points.txt +1 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis_cli.egg-info/requires.txt +1 -1
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/setup.py +2 -2
- mantis_cli-19.2.0/mantis/__init__.py +0 -1
- mantis_cli-19.2.0/mantis/helpers.py +0 -131
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/LICENSE +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/MANIFEST.in +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/README.md +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/__main__.py +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/crypto.py +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/extensions/__init__.py +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/extensions/django.py +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/extensions/nginx.py +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis/extensions/postgres.py +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/mantis_cli.egg-info/top_level.txt +0 -0
- {mantis_cli-19.2.0 → mantis_cli-20.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version:
|
|
2
|
+
Name: mantis_cli
|
|
3
|
+
Version: 20.0.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -9,6 +9,7 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
+
Platform: UNKNOWN
|
|
12
13
|
Classifier: Programming Language :: Python
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
@@ -18,11 +19,6 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
18
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cffi
|
|
22
|
-
Requires-Dist: cryptography
|
|
23
|
-
Requires-Dist: pycryptodome
|
|
24
|
-
Requires-Dist: PyYAML
|
|
25
|
-
Requires-Dist: prettytable
|
|
26
22
|
|
|
27
23
|
# mantis-cli
|
|
28
24
|
|
|
@@ -341,3 +337,4 @@ Works as follows:
|
|
|
341
337
|
## Release notes
|
|
342
338
|
|
|
343
339
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
340
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '20.0.0'
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import inspect
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.text import Text
|
|
6
9
|
|
|
7
10
|
from mantis import VERSION
|
|
8
|
-
from mantis.helpers import
|
|
11
|
+
from mantis.helpers import CLI, nested_set
|
|
9
12
|
from mantis.logic import get_manager, execute
|
|
10
|
-
from mantis.managers import AbstractManager
|
|
13
|
+
from mantis.managers import AbstractManager, BaseManager
|
|
14
|
+
from mantis.extensions.django import Django
|
|
15
|
+
from mantis.extensions.nginx import Nginx
|
|
16
|
+
from mantis.extensions.postgres import Postgres
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
def parse_args(arguments):
|
|
@@ -43,6 +49,9 @@ def run():
|
|
|
43
49
|
if params['commands'] == ['--version']:
|
|
44
50
|
return print(version_info)
|
|
45
51
|
|
|
52
|
+
if params['commands'] == ['--help']:
|
|
53
|
+
return help()
|
|
54
|
+
|
|
46
55
|
# get params
|
|
47
56
|
environment_id = params['environment_id']
|
|
48
57
|
commands = params['commands']
|
|
@@ -51,9 +60,6 @@ def run():
|
|
|
51
60
|
# get manager
|
|
52
61
|
manager = get_manager(environment_id, mode)
|
|
53
62
|
|
|
54
|
-
if params['commands'] == ['--help']:
|
|
55
|
-
return help(manager)
|
|
56
|
-
|
|
57
63
|
if len(params['commands']) == 0:
|
|
58
64
|
CLI.error('Missing commands. Check mantis --help for more information.')
|
|
59
65
|
|
|
@@ -75,25 +81,24 @@ def run():
|
|
|
75
81
|
value=value
|
|
76
82
|
)
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if manager.connection and manager.host:
|
|
81
|
-
host_intro = f'{Colors.RED}{manager.host}{Colors.ENDC}, '
|
|
82
|
-
else:
|
|
83
|
-
host_intro = ''
|
|
84
|
-
|
|
85
|
-
heading = f'{version_info}, '\
|
|
86
|
-
f'{environment_intro}'\
|
|
87
|
-
f'{host_intro}'\
|
|
88
|
-
f'mode: {Colors.GREEN}{manager.mode}{Colors.ENDC}, '\
|
|
89
|
-
f'hostname: {Colors.BLUE}{hostname}{Colors.ENDC}'
|
|
84
|
+
console = Console()
|
|
90
85
|
|
|
91
|
-
|
|
86
|
+
heading = Text.assemble(
|
|
87
|
+
version_info, ", ",
|
|
88
|
+
("Environment ID = ", "") if manager.environment_id else ("(single connection mode), ", "bold") if manager.single_connection_mode else ("", ""),
|
|
89
|
+
(str(manager.environment_id) + ", ", "bold") if manager.environment_id else ("", ""),
|
|
90
|
+
(str(manager.host) + ", ", "red") if manager.connection and manager.host else ("", ""),
|
|
91
|
+
"mode: ", (str(manager.mode), "green"),
|
|
92
|
+
", hostname: ", (hostname, "blue")
|
|
93
|
+
)
|
|
94
|
+
console.print(heading)
|
|
92
95
|
|
|
93
96
|
if mode == 'ssh':
|
|
97
|
+
# Build mantis command - environment_id is optional in single connection mode
|
|
98
|
+
env_part = f'{environment_id} ' if environment_id else ''
|
|
94
99
|
cmds = [
|
|
95
100
|
f'cd {manager.project_path}',
|
|
96
|
-
f'mantis {
|
|
101
|
+
f'mantis {env_part}--mode=host {" ".join(commands)}'
|
|
97
102
|
]
|
|
98
103
|
cmd = ';'.join(cmds)
|
|
99
104
|
exec = f"ssh -t {manager.user}@{manager.host} -p {manager.port} '{cmd}'"
|
|
@@ -109,31 +114,19 @@ def run():
|
|
|
109
114
|
|
|
110
115
|
execute(manager, command, params)
|
|
111
116
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
host \truns mantis on host machine directly without invoking connection (used as proxy for ssh mode)')
|
|
120
|
-
|
|
121
|
-
print(f'\nEnvironment:\n\
|
|
122
|
-
Either "local" or any custom environment identifier defined as connection in your config file.')
|
|
123
|
-
|
|
124
|
-
print(f'\nCommands:')
|
|
125
|
-
|
|
126
|
-
table = prettytable.PrettyTable(align='l')
|
|
127
|
-
table.set_style(prettytable.SINGLE_BORDER)
|
|
128
|
-
table.field_names = ["Command", "Description"]
|
|
117
|
+
def get_class_commands(cls, exclude_from=None):
|
|
118
|
+
"""
|
|
119
|
+
Extract commands from a class for help display.
|
|
120
|
+
Returns list of tuples: (command_str, description)
|
|
121
|
+
"""
|
|
122
|
+
commands = []
|
|
123
|
+
exclude_methods = dir(exclude_from) if exclude_from else []
|
|
129
124
|
|
|
130
|
-
|
|
131
|
-
methods = inspect.getmembers(manager, predicate=inspect.ismethod)
|
|
125
|
+
methods = inspect.getmembers(cls, predicate=inspect.isfunction)
|
|
132
126
|
|
|
133
|
-
# Iterate over each method
|
|
134
127
|
for method_name, method in methods:
|
|
135
|
-
# skip methods
|
|
136
|
-
if method_name in
|
|
128
|
+
# skip private methods and excluded methods
|
|
129
|
+
if method_name.startswith('_') or method_name in exclude_methods:
|
|
137
130
|
continue
|
|
138
131
|
|
|
139
132
|
command = method_name.replace('_', '-')
|
|
@@ -141,33 +134,82 @@ def help(manager):
|
|
|
141
134
|
# Get the method signature
|
|
142
135
|
signature = inspect.signature(method)
|
|
143
136
|
|
|
144
|
-
# Parameters
|
|
145
|
-
parameters =
|
|
137
|
+
# Parameters (skip 'self')
|
|
138
|
+
parameters = [p for p in signature.parameters.keys() if p != 'self']
|
|
146
139
|
|
|
147
140
|
# Check if parameters are optional
|
|
148
141
|
params_are_optional = True
|
|
149
142
|
|
|
150
143
|
for param_name, param in signature.parameters.items():
|
|
151
|
-
if
|
|
144
|
+
if param_name == 'self':
|
|
145
|
+
continue
|
|
146
|
+
if param.default == inspect.Parameter.empty:
|
|
152
147
|
params_are_optional = False
|
|
153
148
|
|
|
154
|
-
#
|
|
149
|
+
# Build command string
|
|
155
150
|
command = f"--{command}"
|
|
156
|
-
|
|
151
|
+
params_str = ""
|
|
157
152
|
|
|
158
|
-
if
|
|
153
|
+
if parameters:
|
|
159
154
|
if not params_are_optional:
|
|
160
|
-
|
|
155
|
+
params_str += '['
|
|
161
156
|
|
|
162
|
-
|
|
157
|
+
params_str += ':'
|
|
163
158
|
|
|
164
|
-
|
|
159
|
+
params_str += ','.join(parameters)
|
|
165
160
|
|
|
166
161
|
if not params_are_optional:
|
|
167
|
-
|
|
162
|
+
params_str += ']'
|
|
168
163
|
|
|
169
164
|
docs = method.__doc__ or ''
|
|
170
165
|
|
|
171
|
-
|
|
166
|
+
commands.append((f"{command}{params_str}", docs.strip()))
|
|
167
|
+
|
|
168
|
+
return commands
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def help():
|
|
172
|
+
print(f'\nUsage:\n\
|
|
173
|
+
mantis [--mode=remote|ssh|host] [environment] --command[:params]')
|
|
174
|
+
|
|
175
|
+
print('\nModes:\n\
|
|
176
|
+
remote \truns commands remotely from local machine using DOCKER_HOST or DOCKER_CONTEXT (default)\n\
|
|
177
|
+
ssh \tconnects to host via ssh and run all mantis commands on remote machine directly (mantis-cli needs to be installed on server)\n\
|
|
178
|
+
host \truns mantis on host machine directly without invoking connection (used as proxy for ssh mode)')
|
|
179
|
+
|
|
180
|
+
print(f'\nEnvironment:\n\
|
|
181
|
+
Either "local" or any custom environment identifier defined as connection in your config file.\n\
|
|
182
|
+
Optional when using single connection mode (config has "connection" instead of "connections").')
|
|
183
|
+
|
|
184
|
+
console = Console()
|
|
172
185
|
|
|
173
|
-
|
|
186
|
+
# Base commands
|
|
187
|
+
print(f'\nCommands:')
|
|
188
|
+
table = Table(show_header=True, header_style="bold")
|
|
189
|
+
table.add_column("Command", style="cyan")
|
|
190
|
+
table.add_column("Description")
|
|
191
|
+
|
|
192
|
+
for command, description in get_class_commands(BaseManager, exclude_from=AbstractManager):
|
|
193
|
+
table.add_row(command, description)
|
|
194
|
+
|
|
195
|
+
console.print(table)
|
|
196
|
+
|
|
197
|
+
# Extension commands
|
|
198
|
+
extensions = [
|
|
199
|
+
('Django', Django),
|
|
200
|
+
('Nginx', Nginx),
|
|
201
|
+
('Postgres', Postgres),
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for ext_name, ext_class in extensions:
|
|
205
|
+
ext_commands = get_class_commands(ext_class)
|
|
206
|
+
if ext_commands:
|
|
207
|
+
print(f'\n{ext_name} extension:')
|
|
208
|
+
ext_table = Table(show_header=True, header_style="bold")
|
|
209
|
+
ext_table.add_column("Command", style="yellow")
|
|
210
|
+
ext_table.add_column("Description")
|
|
211
|
+
|
|
212
|
+
for command, description in ext_commands:
|
|
213
|
+
ext_table.add_row(command, description)
|
|
214
|
+
|
|
215
|
+
console.print(ext_table)
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from mantis.helpers import CLI
|
|
3
|
+
from mantis.helpers import CLI
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Environment(object):
|
|
7
|
-
def __init__(self, environment_id, folder):
|
|
7
|
+
def __init__(self, environment_id, folder, single_mode=False):
|
|
8
8
|
self.id = environment_id
|
|
9
9
|
self.folder = folder
|
|
10
|
+
self.single_mode = single_mode
|
|
10
11
|
|
|
11
|
-
if self.
|
|
12
|
+
if self.single_mode:
|
|
13
|
+
self.setup_single_mode()
|
|
14
|
+
elif self.id:
|
|
12
15
|
self.setup()
|
|
13
16
|
|
|
14
17
|
def setup(self):
|
|
@@ -30,6 +33,31 @@ class Environment(object):
|
|
|
30
33
|
self.files = list(map(lambda x: os.path.join(dirpath, x), environment_filenames))
|
|
31
34
|
self.encrypted_files = list(map(lambda x: os.path.join(dirpath, x), encrypted_environment_filenames))
|
|
32
35
|
|
|
36
|
+
def setup_single_mode(self):
|
|
37
|
+
"""
|
|
38
|
+
Setup for single connection mode: look for env files directly in the folder
|
|
39
|
+
instead of environment subfolders
|
|
40
|
+
"""
|
|
41
|
+
self.path = self.folder
|
|
42
|
+
|
|
43
|
+
if not os.path.exists(self.path):
|
|
44
|
+
CLI.warning(f"Environment path '{self.path}' does not exist")
|
|
45
|
+
self.files = []
|
|
46
|
+
self.encrypted_files = []
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if not os.path.isdir(self.path):
|
|
50
|
+
CLI.error(f"Environment path '{self.path}' is not directory")
|
|
51
|
+
|
|
52
|
+
CLI.info(f"Found environment path (single mode): '{self.path}'")
|
|
53
|
+
|
|
54
|
+
# Look for env files directly in the folder (not in subdirectories)
|
|
55
|
+
files = os.listdir(self.path)
|
|
56
|
+
environment_filenames = list(filter(lambda f: f.endswith('.env') and not f.endswith('.encrypted'), files))
|
|
57
|
+
encrypted_environment_filenames = list(filter(lambda f: f.endswith('.env.encrypted'), files))
|
|
58
|
+
self.files = list(map(lambda x: os.path.join(self.path, x), environment_filenames))
|
|
59
|
+
self.encrypted_files = list(map(lambda x: os.path.join(self.path, x), encrypted_environment_filenames))
|
|
60
|
+
|
|
33
61
|
def _get_path(self, id):
|
|
34
62
|
possible_folder_names = [f'.{id}', id]
|
|
35
63
|
possible_folders = list(map(lambda x: os.path.normpath(os.path.join(self.folder, x)), possible_folder_names))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.text import Text
|
|
3
|
+
|
|
4
|
+
# Shared console instance
|
|
5
|
+
_console = Console()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CLI(object):
|
|
9
|
+
@staticmethod
|
|
10
|
+
def _print(text, style, end='\n'):
|
|
11
|
+
styled_text = Text(str(text), style=style)
|
|
12
|
+
_console.print(styled_text, end=end)
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def error(text):
|
|
16
|
+
styled_text = Text(str(text), style='red')
|
|
17
|
+
_console.print(styled_text)
|
|
18
|
+
exit(1)
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def bold(text, end='\n'):
|
|
22
|
+
return CLI._print(text=text, style='bold', end=end)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def info(text, end='\n'):
|
|
26
|
+
return CLI._print(text=text, style='blue', end=end)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def pink(text, end='\n'):
|
|
30
|
+
return CLI._print(text=text, style='magenta', end=end)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def success(text, end='\n'):
|
|
34
|
+
return CLI._print(text=text, style='green', end=end)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def warning(text, end='\n'):
|
|
38
|
+
return CLI._print(text=text, style='yellow', end=end)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def danger(text, end='\n'):
|
|
42
|
+
return CLI._print(text=text, style='red', end=end)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def underline(text, end='\n'):
|
|
46
|
+
return CLI._print(text=text, style='underline', end=end)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def step(index, total, text, end='\n'):
|
|
50
|
+
return CLI._print(text=f'[{index}/{total}] {text}', style='yellow', end=end)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def link(uri, label=None):
|
|
54
|
+
if label is None:
|
|
55
|
+
label = uri
|
|
56
|
+
return f'[link={uri}]{label}[/link]'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def nested_set(dic, keys, value):
|
|
60
|
+
for key in keys[:-1]:
|
|
61
|
+
dic = dic.setdefault(key, {})
|
|
62
|
+
dic[keys[-1]] = value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def import_string(path):
|
|
66
|
+
components = path.split('.')
|
|
67
|
+
mod = __import__('.'.join(components[0:-1]), globals(), locals(), [components[-1]])
|
|
68
|
+
return getattr(mod, components[-1])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def random_string(n=10):
|
|
72
|
+
import random
|
|
73
|
+
import string
|
|
74
|
+
|
|
75
|
+
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
|
|
76
|
+
return ''.join(random.choice(chars) for _ in range(n))
|
|
77
|
+
|
|
78
|
+
def merge_json(obj1, obj2):
|
|
79
|
+
# Base case: if both values are dictionaries, merge recursively
|
|
80
|
+
if isinstance(obj1, dict) and isinstance(obj2, dict):
|
|
81
|
+
merged = {}
|
|
82
|
+
for key in obj1.keys() | obj2.keys(): # Union of both sets of keys
|
|
83
|
+
if key in obj1 and key in obj2:
|
|
84
|
+
merged[key] = merge_json(obj1[key], obj2[key])
|
|
85
|
+
elif key in obj1:
|
|
86
|
+
merged[key] = obj1[key]
|
|
87
|
+
else:
|
|
88
|
+
merged[key] = obj2[key]
|
|
89
|
+
return merged
|
|
90
|
+
# If both are lists, combine them
|
|
91
|
+
elif isinstance(obj1, list) and isinstance(obj2, list):
|
|
92
|
+
return obj1 + obj2
|
|
93
|
+
# If both values are not dicts or lists, return value from obj2
|
|
94
|
+
else:
|
|
95
|
+
if obj1 == obj2:
|
|
96
|
+
return obj1
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(f'Trying to merge objects: {obj1} and {obj2}')
|
|
@@ -2,7 +2,9 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
from json.decoder import JSONDecodeError
|
|
4
4
|
from os.path import dirname, normpath, abspath
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
6
8
|
|
|
7
9
|
from mantis.helpers import CLI, import_string
|
|
8
10
|
|
|
@@ -37,23 +39,36 @@ def find_config(environment_id=None):
|
|
|
37
39
|
# Multiple mantis files found
|
|
38
40
|
CLI.info(f'Found {total_mantis_files} mantis.json files:')
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
table
|
|
42
|
+
console = Console()
|
|
43
|
+
table = Table(show_header=True, header_style="bold")
|
|
44
|
+
table.add_column("#", style="cyan")
|
|
45
|
+
table.add_column("Path")
|
|
46
|
+
table.add_column("Connections")
|
|
42
47
|
|
|
43
48
|
for index, path in enumerate(paths):
|
|
44
49
|
config = load_config(path)
|
|
45
|
-
connections = config.get('connections', {}).keys()
|
|
46
50
|
|
|
47
|
-
#
|
|
51
|
+
# Check for single connection mode
|
|
52
|
+
single_connection = config.get('connection')
|
|
53
|
+
|
|
54
|
+
if single_connection:
|
|
55
|
+
# Single connection mode - display the connection string
|
|
56
|
+
connections_display = '[green](single)[/green]'
|
|
57
|
+
else:
|
|
58
|
+
# Multi-environment mode - display connection keys
|
|
59
|
+
connections = config.get('connections', {}).keys()
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
for connection in connections:
|
|
51
|
-
color = 'success' if connection == environment_id else 'warning'
|
|
52
|
-
colorful_connections.append(getattr(CLI, color)(connection, end='', return_value=True))
|
|
61
|
+
# TODO: get project names from compose files
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
colorful_connections = []
|
|
64
|
+
for connection in connections:
|
|
65
|
+
color = 'green' if connection == environment_id else 'yellow'
|
|
66
|
+
colorful_connections.append(f'[{color}]{connection}[/{color}]')
|
|
67
|
+
connections_display = ', '.join(colorful_connections)
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
table.add_row(str(index + 1), normpath(dirname(path)), connections_display)
|
|
70
|
+
|
|
71
|
+
console.print(table)
|
|
57
72
|
CLI.danger(f'[0] Exit now and define $MANTIS_CONFIG environment variable')
|
|
58
73
|
|
|
59
74
|
path_index = None
|
|
@@ -92,8 +107,11 @@ def find_keys_only_in_config(config, template, parent_key=""):
|
|
|
92
107
|
|
|
93
108
|
def load_config(config_file):
|
|
94
109
|
if not os.path.exists(config_file):
|
|
95
|
-
CLI.warning(f'File {config_file} does not exist.
|
|
96
|
-
|
|
110
|
+
CLI.warning(f'File {config_file} does not exist.')
|
|
111
|
+
CLI.danger(f'Mantis config not found. Double check your current working directory.')
|
|
112
|
+
exit()
|
|
113
|
+
# CLI.warning(f'File {config_file} does not exist. Returning empty config')
|
|
114
|
+
# return {}
|
|
97
115
|
|
|
98
116
|
with open(config_file, "r") as config:
|
|
99
117
|
try:
|
|
@@ -191,7 +209,8 @@ def execute(manager, command, params):
|
|
|
191
209
|
else:
|
|
192
210
|
methods_without_environment = ['contexts', 'create_context', 'check_config', 'generate_key', 'read_key']
|
|
193
211
|
|
|
194
|
-
|
|
212
|
+
# In single connection mode, environment_id is not required
|
|
213
|
+
if manager.environment_id is None and not manager.single_connection_mode and manager_method not in methods_without_environment:
|
|
195
214
|
CLI.error('Missing environment')
|
|
196
215
|
elif manager.environment_id is not None and manager_method in methods_without_environment:
|
|
197
216
|
CLI.error('Redundant environment')
|
|
@@ -8,9 +8,12 @@ from os import path
|
|
|
8
8
|
from os.path import normpath
|
|
9
9
|
from time import sleep
|
|
10
10
|
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
11
14
|
from mantis.crypto import Crypto
|
|
12
15
|
from mantis.environment import Environment
|
|
13
|
-
from mantis.helpers import CLI,
|
|
16
|
+
from mantis.helpers import CLI, merge_json
|
|
14
17
|
from mantis.logic import find_config, load_config, check_config, load_template_config
|
|
15
18
|
|
|
16
19
|
|
|
@@ -62,7 +65,8 @@ class AbstractManager(object):
|
|
|
62
65
|
|
|
63
66
|
@property
|
|
64
67
|
def connection_details(self):
|
|
65
|
-
|
|
68
|
+
# In single connection mode, env.id is None but we still have a connection
|
|
69
|
+
if not self.single_connection_mode and self.env.id is None:
|
|
66
70
|
return None
|
|
67
71
|
|
|
68
72
|
property_name = '_connection_details'
|
|
@@ -75,7 +79,7 @@ class AbstractManager(object):
|
|
|
75
79
|
if hasattr(self, property_name):
|
|
76
80
|
return getattr(self, property_name)
|
|
77
81
|
|
|
78
|
-
if 'local' in self.env.id:
|
|
82
|
+
if self.env.id and 'local' in self.env.id:
|
|
79
83
|
details = {
|
|
80
84
|
'host': 'localhost',
|
|
81
85
|
'user': None,
|
|
@@ -109,12 +113,14 @@ class AbstractManager(object):
|
|
|
109
113
|
|
|
110
114
|
@property
|
|
111
115
|
def docker_connection(self):
|
|
112
|
-
|
|
116
|
+
# In single connection mode or when env.id contains 'local', no extra connection needed
|
|
117
|
+
if not self.single_connection_mode and (self.env.id is None or 'local' in self.env.id):
|
|
113
118
|
return ''
|
|
114
119
|
|
|
115
120
|
if self.mode == 'remote':
|
|
116
121
|
if self.connection is None:
|
|
117
|
-
|
|
122
|
+
env_info = f' for environment {self.env.id}' if self.env.id else ''
|
|
123
|
+
CLI.error(f'Connection{env_info} not defined!')
|
|
118
124
|
if self.connection.startswith('ssh://'):
|
|
119
125
|
return f'DOCKER_HOST="{self.connection}"'
|
|
120
126
|
elif self.connection.startswith('context://'):
|
|
@@ -139,14 +145,56 @@ class AbstractManager(object):
|
|
|
139
145
|
# Save merged config to variable
|
|
140
146
|
self.config = defaults.copy()
|
|
141
147
|
|
|
148
|
+
# Detect single connection mode (connection string instead of connections dict)
|
|
149
|
+
has_single_connection = self.config.get('connection') is not None
|
|
150
|
+
has_multiple_connections = bool(self.config.get('connections', {}))
|
|
151
|
+
|
|
152
|
+
# Validate: only one of connection or connections should be defined
|
|
153
|
+
if has_single_connection and has_multiple_connections:
|
|
154
|
+
CLI.error('Config error: Cannot define both "connection" and "connections". Use either single connection mode or named environments, not both.')
|
|
155
|
+
|
|
156
|
+
self.single_connection_mode = has_single_connection
|
|
157
|
+
|
|
158
|
+
# Validate: environment_id should not be provided in single connection mode
|
|
159
|
+
if self.single_connection_mode and self.environment_id:
|
|
160
|
+
CLI.error(f'Config error: Environment "{self.environment_id}" was provided, but config uses single connection mode. Remove the environment argument or switch to named environments using "connections".')
|
|
161
|
+
|
|
142
162
|
self.key_file = normalize(path.join(self.config['encryption']['folder'], 'mantis.key'))
|
|
143
163
|
self.environment_path = normalize(self.config['environment']['folder'])
|
|
144
164
|
|
|
145
|
-
if self.
|
|
165
|
+
if self.single_connection_mode:
|
|
166
|
+
# In single connection mode, compose files are directly in compose folder
|
|
167
|
+
self.compose_path = normalize(self.config['compose']['folder'])
|
|
168
|
+
elif self.environment_id:
|
|
146
169
|
self.compose_path = normalize(path.join(self.config['compose']['folder'], self.environment_id))
|
|
147
170
|
|
|
148
171
|
def init_environment(self):
|
|
172
|
+
if self.single_connection_mode:
|
|
173
|
+
# Single connection mode: no environment_id required
|
|
174
|
+
self.env = Environment(
|
|
175
|
+
environment_id=None,
|
|
176
|
+
folder=self.environment_path,
|
|
177
|
+
single_mode=True,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# connection from single 'connection' key
|
|
181
|
+
self.connection = self.config.get('connection')
|
|
182
|
+
|
|
183
|
+
# compose files directly in compose folder
|
|
184
|
+
compose_file_paths = os.popen(f'find {self.compose_path} -maxdepth 1 -name "*.yml" -o -name "*.yaml"').read().strip().split('\n')
|
|
185
|
+
|
|
186
|
+
# Remove empty strings
|
|
187
|
+
self.compose_files = list(filter(None, compose_file_paths))
|
|
188
|
+
|
|
189
|
+
# Read compose files
|
|
190
|
+
self.compose_config = self.read_compose_configs()
|
|
191
|
+
return
|
|
192
|
+
|
|
149
193
|
if not self.environment_id:
|
|
194
|
+
self.env = Environment(
|
|
195
|
+
environment_id=None,
|
|
196
|
+
folder=self.environment_path,
|
|
197
|
+
)
|
|
150
198
|
self.connection = None
|
|
151
199
|
return
|
|
152
200
|
|
|
@@ -167,6 +215,30 @@ class AbstractManager(object):
|
|
|
167
215
|
# Read compose files
|
|
168
216
|
self.compose_config = self.read_compose_configs()
|
|
169
217
|
|
|
218
|
+
def are_env_files_in_sync(self, env_file):
|
|
219
|
+
"""
|
|
220
|
+
Checks if .env and .env.encrypted files are in sync.
|
|
221
|
+
Returns True if they match, False otherwise.
|
|
222
|
+
"""
|
|
223
|
+
env_file_encrypted = f'{env_file}.encrypted'
|
|
224
|
+
|
|
225
|
+
# Check if both files exist
|
|
226
|
+
if not os.path.exists(env_file):
|
|
227
|
+
return False
|
|
228
|
+
if not os.path.exists(env_file_encrypted):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
decrypted_environment = self.decrypt_env(env_file=env_file, return_value=True)
|
|
233
|
+
loaded_environment = self.env.load(env_file)
|
|
234
|
+
|
|
235
|
+
if decrypted_environment is None or loaded_environment is None:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
return loaded_environment == decrypted_environment
|
|
239
|
+
except Exception:
|
|
240
|
+
return False
|
|
241
|
+
|
|
170
242
|
def check_environment_encryption(self, env_file):
|
|
171
243
|
decrypted_environment = self.decrypt_env(env_file=env_file, return_value=True) # .env.encrypted
|
|
172
244
|
loaded_environment = self.env.load(env_file) # .env
|
|
@@ -345,9 +417,15 @@ class BaseManager(AbstractManager):
|
|
|
345
417
|
|
|
346
418
|
return values if return_value else None
|
|
347
419
|
|
|
348
|
-
CLI.info(f'Encrypting environment file {env_file}...')
|
|
349
420
|
env_file_encrypted = f'{env_file}.encrypted'
|
|
350
421
|
|
|
422
|
+
# Skip if files are already in sync (unless return_value is True, which is used for internal checks)
|
|
423
|
+
if not return_value and self.are_env_files_in_sync(env_file):
|
|
424
|
+
CLI.success(f'Skipping {env_file} - already in sync with {env_file_encrypted}')
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
CLI.info(f'Encrypting environment file {env_file}...')
|
|
428
|
+
|
|
351
429
|
if not self.KEY:
|
|
352
430
|
CLI.error('Missing mantis key! (%s)' % self.key_file)
|
|
353
431
|
|
|
@@ -408,6 +486,11 @@ class BaseManager(AbstractManager):
|
|
|
408
486
|
|
|
409
487
|
env_file_encrypted = f'{env_file}.encrypted'
|
|
410
488
|
|
|
489
|
+
# Skip if files are already in sync (unless return_value is True, which is used for internal checks)
|
|
490
|
+
if not return_value and self.are_env_files_in_sync(env_file):
|
|
491
|
+
CLI.success(f'Skipping {env_file_encrypted} - already in sync with {env_file}')
|
|
492
|
+
return None
|
|
493
|
+
|
|
411
494
|
if not return_value:
|
|
412
495
|
CLI.info(f'Decrypting environment file {env_file_encrypted}...')
|
|
413
496
|
|
|
@@ -614,7 +697,8 @@ class BaseManager(AbstractManager):
|
|
|
614
697
|
if container not in self.get_containers():
|
|
615
698
|
CLI.error(f"Container {container} not found")
|
|
616
699
|
|
|
617
|
-
|
|
700
|
+
console = Console()
|
|
701
|
+
console.print(f'[blue]Health-checking [yellow]{container}[/yellow]...[/blue]')
|
|
618
702
|
|
|
619
703
|
if self.has_healthcheck(container):
|
|
620
704
|
healthcheck_config = self.get_healthcheck_config(container)
|
|
@@ -624,8 +708,8 @@ class BaseManager(AbstractManager):
|
|
|
624
708
|
interval = healthcheck_interval / coeficient
|
|
625
709
|
retries = healthcheck_retries * coeficient
|
|
626
710
|
|
|
627
|
-
|
|
628
|
-
|
|
711
|
+
console.print(f'[blue]Interval: [dim]{healthcheck_interval}[/dim] s -> [yellow]{interval} s[/yellow][/blue]')
|
|
712
|
+
console.print(f'[blue]Retries: [dim]{healthcheck_retries}[/dim] -> [yellow]{retries}[/yellow][/blue]')
|
|
629
713
|
|
|
630
714
|
start = time.time()
|
|
631
715
|
|
|
@@ -633,13 +717,13 @@ class BaseManager(AbstractManager):
|
|
|
633
717
|
is_healthy, status = self.check_health(container)
|
|
634
718
|
|
|
635
719
|
if is_healthy:
|
|
636
|
-
print(f"#{retry + 1}/{retries}: Status of '{container}' is {
|
|
720
|
+
console.print(f"#{retry + 1}/{retries}: Status of '{container}' is [green]{status}[/green].")
|
|
637
721
|
end = time.time()
|
|
638
722
|
loading_time = end - start
|
|
639
|
-
print(f'Container {
|
|
723
|
+
console.print(f'Container [yellow]{container}[/yellow] took [blue underline]{loading_time} s[/blue underline] to become healthy')
|
|
640
724
|
return True
|
|
641
725
|
else:
|
|
642
|
-
print(f"#{retry + 1}/{retries}: Status of '{container}' is {
|
|
726
|
+
console.print(f"#{retry + 1}/{retries}: Status of '{container}' is [red]{status}[/red].")
|
|
643
727
|
|
|
644
728
|
if retries > 1:
|
|
645
729
|
sleep(interval)
|
|
@@ -1089,14 +1173,73 @@ class BaseManager(AbstractManager):
|
|
|
1089
1173
|
"""
|
|
1090
1174
|
Prints images and containers
|
|
1091
1175
|
"""
|
|
1176
|
+
console = Console()
|
|
1177
|
+
|
|
1092
1178
|
CLI.info('Getting status...')
|
|
1093
1179
|
steps = 2
|
|
1094
1180
|
|
|
1095
1181
|
CLI.step(1, steps, 'List of Docker images')
|
|
1096
|
-
self.docker(
|
|
1182
|
+
images_output = self.docker('image ls --format "{{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"', return_output=True)
|
|
1183
|
+
|
|
1184
|
+
if images_output.strip():
|
|
1185
|
+
images_table = Table(show_header=True, header_style="bold")
|
|
1186
|
+
images_table.add_column("REPOSITORY", style="cyan")
|
|
1187
|
+
images_table.add_column("TAG", style="yellow")
|
|
1188
|
+
images_table.add_column("IMAGE ID", style="bright_blue")
|
|
1189
|
+
images_table.add_column("CREATED", style="magenta")
|
|
1190
|
+
images_table.add_column("SIZE", style="green")
|
|
1191
|
+
|
|
1192
|
+
for line in images_output.strip().split('\n'):
|
|
1193
|
+
parts = line.split('\t')
|
|
1194
|
+
if len(parts) >= 5:
|
|
1195
|
+
repo, tag, image_id, created, size = parts[0], parts[1], parts[2], parts[3], parts[4]
|
|
1196
|
+
images_table.add_row(repo, tag, image_id, created, size)
|
|
1197
|
+
|
|
1198
|
+
console.print(images_table)
|
|
1097
1199
|
|
|
1098
1200
|
CLI.step(2, steps, 'Docker containers')
|
|
1099
|
-
self.docker(
|
|
1201
|
+
containers_output = self.docker('container ls -a --format "{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}\t{{.Size}}"', return_output=True)
|
|
1202
|
+
|
|
1203
|
+
if containers_output.strip():
|
|
1204
|
+
containers_table = Table(show_header=True, header_style="bold")
|
|
1205
|
+
containers_table.add_column("NAME", style="blue")
|
|
1206
|
+
containers_table.add_column("STATUS")
|
|
1207
|
+
containers_table.add_column("IMAGE", style="magenta")
|
|
1208
|
+
containers_table.add_column("PORTS")
|
|
1209
|
+
containers_table.add_column("SIZE", style="dark_orange")
|
|
1210
|
+
|
|
1211
|
+
for line in containers_output.strip().split('\n'):
|
|
1212
|
+
parts = line.split('\t')
|
|
1213
|
+
if len(parts) >= 5:
|
|
1214
|
+
name, status, image, ports, size = parts[0], parts[1], parts[2], parts[3], parts[4]
|
|
1215
|
+
|
|
1216
|
+
# Colorize status based on state
|
|
1217
|
+
if 'Up' in status:
|
|
1218
|
+
status_colored = f'[green]{status}[/green]'
|
|
1219
|
+
elif 'Exited' in status:
|
|
1220
|
+
status_colored = f'[red]{status}[/red]'
|
|
1221
|
+
elif 'Created' in status:
|
|
1222
|
+
status_colored = f'[yellow]{status}[/yellow]'
|
|
1223
|
+
elif 'Paused' in status:
|
|
1224
|
+
status_colored = f'[yellow]{status}[/yellow]'
|
|
1225
|
+
else:
|
|
1226
|
+
status_colored = status
|
|
1227
|
+
|
|
1228
|
+
# Split ports into multiple lines with different colors for IPv4/IPv6
|
|
1229
|
+
ports_list = ports.split(', ') if ports else ['']
|
|
1230
|
+
colored_ports = []
|
|
1231
|
+
for port in ports_list:
|
|
1232
|
+
if '::' in port or '[' in port:
|
|
1233
|
+
# IPv6 port
|
|
1234
|
+
colored_ports.append(f'[bright_white]{port}[/bright_white]')
|
|
1235
|
+
else:
|
|
1236
|
+
# IPv4 port
|
|
1237
|
+
colored_ports.append(f'[cyan]{port}[/cyan]')
|
|
1238
|
+
ports_formatted = '\n'.join(colored_ports)
|
|
1239
|
+
|
|
1240
|
+
containers_table.add_row(name, status_colored, image, ports_formatted, size)
|
|
1241
|
+
|
|
1242
|
+
console.print(containers_table)
|
|
1100
1243
|
|
|
1101
1244
|
def networks(self):
|
|
1102
1245
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mantis-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 20.0.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -9,6 +9,7 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
+
Platform: UNKNOWN
|
|
12
13
|
Classifier: Programming Language :: Python
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
@@ -18,11 +19,6 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
18
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cffi
|
|
22
|
-
Requires-Dist: cryptography
|
|
23
|
-
Requires-Dist: pycryptodome
|
|
24
|
-
Requires-Dist: PyYAML
|
|
25
|
-
Requires-Dist: prettytable
|
|
26
22
|
|
|
27
23
|
# mantis-cli
|
|
28
24
|
|
|
@@ -341,3 +337,4 @@ Works as follows:
|
|
|
341
337
|
## Release notes
|
|
342
338
|
|
|
343
339
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
340
|
+
|
|
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
|
|
|
4
4
|
from mantis import VERSION
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
|
-
name='
|
|
7
|
+
name='mantis_cli',
|
|
8
8
|
version=VERSION,
|
|
9
9
|
description='Management command to build and deploy webapps, especially based on Django',
|
|
10
10
|
long_description=open('README.md').read(),
|
|
@@ -16,7 +16,7 @@ setup(
|
|
|
16
16
|
url='https://github.com/PragmaticMates/mantis-cli',
|
|
17
17
|
packages=find_packages(),
|
|
18
18
|
include_package_data=True,
|
|
19
|
-
install_requires=['cffi', 'cryptography', 'pycryptodome', 'PyYAML', '
|
|
19
|
+
install_requires=['cffi', 'cryptography', 'pycryptodome', 'PyYAML', 'rich'],
|
|
20
20
|
entry_points={
|
|
21
21
|
'console_scripts': ['mantis=mantis.command_line:run'],
|
|
22
22
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '19.2.0'
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
class Colors:
|
|
2
|
-
# https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux
|
|
3
|
-
BLACK = "\033[0;30m"
|
|
4
|
-
BLUE = '\033[94m'
|
|
5
|
-
# BLUE = "\033[0;34m"
|
|
6
|
-
GREEN = '\033[92m'
|
|
7
|
-
# GREEN = "\033[0;32m"
|
|
8
|
-
YELLOW = '\033[93m'
|
|
9
|
-
# YELLOW = "\033[1;33m"
|
|
10
|
-
RED = '\033[91m'
|
|
11
|
-
# RED = "\033[0;31m"
|
|
12
|
-
PINK = '\033[95m'
|
|
13
|
-
BOLD = '\033[1m'
|
|
14
|
-
UNDERLINE = '\033[4m'
|
|
15
|
-
BROWN = "\033[0;33m"
|
|
16
|
-
PURPLE = "\033[0;35m"
|
|
17
|
-
CYAN = "\033[0;36m"
|
|
18
|
-
LIGHT_GRAY = "\033[0;37m"
|
|
19
|
-
DARK_GRAY = "\033[1;30m"
|
|
20
|
-
LIGHT_RED = "\033[1;31m"
|
|
21
|
-
LIGHT_GREEN = "\033[1;32m"
|
|
22
|
-
LIGHT_BLUE = "\033[1;34m"
|
|
23
|
-
LIGHT_PURPLE = "\033[1;35m"
|
|
24
|
-
LIGHT_CYAN = "\033[1;36m"
|
|
25
|
-
LIGHT_WHITE = "\033[1;37m"
|
|
26
|
-
FAINT = "\033[2m"
|
|
27
|
-
ITALIC = "\033[3m"
|
|
28
|
-
BLINK_SLOW = "\033[5m"
|
|
29
|
-
BLINK_FAST = "\033[6m"
|
|
30
|
-
NEGATIVE = "\033[7m"
|
|
31
|
-
CROSSED = "\033[9m"
|
|
32
|
-
RESET = "\033[0m"
|
|
33
|
-
ENDC = '\033[0m'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class CLI(object):
|
|
37
|
-
@staticmethod
|
|
38
|
-
def print_or_return(text, color, end='\n', return_value=False):
|
|
39
|
-
s = f'{color}{text}{Colors.ENDC}'
|
|
40
|
-
if return_value:
|
|
41
|
-
return f'{s}{end}'
|
|
42
|
-
print(s, end=end)
|
|
43
|
-
|
|
44
|
-
@staticmethod
|
|
45
|
-
def error(text):
|
|
46
|
-
exit(f'{Colors.RED}{text}{Colors.ENDC}')
|
|
47
|
-
|
|
48
|
-
@staticmethod
|
|
49
|
-
def bold(text, end='\n', return_value=False):
|
|
50
|
-
return CLI.print_or_return(text=text, color=Colors.BOLD, end=end, return_value=return_value)
|
|
51
|
-
|
|
52
|
-
@staticmethod
|
|
53
|
-
def info(text, end='\n', return_value=False):
|
|
54
|
-
return CLI.print_or_return(text=text, color=Colors.BLUE, end=end, return_value=return_value)
|
|
55
|
-
|
|
56
|
-
@staticmethod
|
|
57
|
-
def pink(text, end='\n', return_value=False):
|
|
58
|
-
return CLI.print_or_return(text=text, color=Colors.PINK, end=end, return_value=return_value)
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def success(text, end='\n', return_value=False):
|
|
62
|
-
return CLI.print_or_return(text=text, color=Colors.GREEN, end=end, return_value=return_value)
|
|
63
|
-
|
|
64
|
-
@staticmethod
|
|
65
|
-
def warning(text, end='\n', return_value=False):
|
|
66
|
-
return CLI.print_or_return(text=text, color=Colors.YELLOW, end=end, return_value=return_value)
|
|
67
|
-
|
|
68
|
-
@staticmethod
|
|
69
|
-
def danger(text, end='\n', return_value=False):
|
|
70
|
-
return CLI.print_or_return(text=text, color=Colors.RED, end=end, return_value=return_value)
|
|
71
|
-
|
|
72
|
-
@staticmethod
|
|
73
|
-
def underline(text, end='\n', return_value=False):
|
|
74
|
-
return CLI.print_or_return(text=text, color=Colors.UNDERLINE, end=end, return_value=return_value)
|
|
75
|
-
|
|
76
|
-
@staticmethod
|
|
77
|
-
def step(index, total, text, end='\n', return_value=False):
|
|
78
|
-
return CLI.print_or_return(text=f'[{index}/{total}] {text}', color=Colors.YELLOW, end=end, return_value=return_value)
|
|
79
|
-
|
|
80
|
-
@staticmethod
|
|
81
|
-
def link(uri, label=None):
|
|
82
|
-
if label is None:
|
|
83
|
-
label = uri
|
|
84
|
-
parameters = ''
|
|
85
|
-
|
|
86
|
-
# OSC 8 ; params ; URI ST <name> OSC 8 ;; ST
|
|
87
|
-
escape_mask = '\033]8;{};{}\033\\{}\033]8;;\033\\'
|
|
88
|
-
|
|
89
|
-
return escape_mask.format(parameters, uri, label)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def nested_set(dic, keys, value):
|
|
93
|
-
for key in keys[:-1]:
|
|
94
|
-
dic = dic.setdefault(key, {})
|
|
95
|
-
dic[keys[-1]] = value
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def import_string(path):
|
|
99
|
-
components = path.split('.')
|
|
100
|
-
mod = __import__('.'.join(components[0:-1]), globals(), locals(), [components[-1]])
|
|
101
|
-
return getattr(mod, components[-1])
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def random_string(n=10):
|
|
105
|
-
import random
|
|
106
|
-
import string
|
|
107
|
-
|
|
108
|
-
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
|
|
109
|
-
return ''.join(random.choice(chars) for _ in range(n))
|
|
110
|
-
|
|
111
|
-
def merge_json(obj1, obj2):
|
|
112
|
-
# Base case: if both values are dictionaries, merge recursively
|
|
113
|
-
if isinstance(obj1, dict) and isinstance(obj2, dict):
|
|
114
|
-
merged = {}
|
|
115
|
-
for key in obj1.keys() | obj2.keys(): # Union of both sets of keys
|
|
116
|
-
if key in obj1 and key in obj2:
|
|
117
|
-
merged[key] = merge_json(obj1[key], obj2[key])
|
|
118
|
-
elif key in obj1:
|
|
119
|
-
merged[key] = obj1[key]
|
|
120
|
-
else:
|
|
121
|
-
merged[key] = obj2[key]
|
|
122
|
-
return merged
|
|
123
|
-
# If both are lists, combine them
|
|
124
|
-
elif isinstance(obj1, list) and isinstance(obj2, list):
|
|
125
|
-
return obj1 + obj2
|
|
126
|
-
# If both values are not dicts or lists, return value from obj2
|
|
127
|
-
else:
|
|
128
|
-
if obj1 == obj2:
|
|
129
|
-
return obj1
|
|
130
|
-
else:
|
|
131
|
-
raise ValueError(f'Trying to merge objects: {obj1} and {obj2}')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|