muscles-cli 1.0.0__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.
- muscles/cli/Makefile +26 -0
- muscles/cli/__about__.py +6 -0
- muscles/cli/__init__.py +66 -0
- muscles/cli/__main__.py +5 -0
- muscles/cli/cli/__init__.py +6 -0
- muscles/cli/cli/cli.py +44 -0
- muscles/cli/cli/colory.py +15 -0
- muscles/cli/cli/command.py +778 -0
- muscles/cli/cli/constants.py +3 -0
- muscles/cli/cli/error_handler.py +6 -0
- muscles/cli/cli/func.py +221 -0
- muscles/cli/cli/instance.py +47 -0
- muscles/cli/cli/strategy.py +111 -0
- muscles/cli/providers.py +198 -0
- muscles/cli/streaming.py +50 -0
- muscles/cli/tooling.py +927 -0
- muscles_cli-1.0.0.dist-info/METADATA +165 -0
- muscles_cli-1.0.0.dist-info/RECORD +20 -0
- muscles_cli-1.0.0.dist-info/WHEEL +4 -0
- muscles_cli-1.0.0.dist-info/entry_points.txt +3 -0
muscles/cli/cli/func.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import threading
|
|
3
|
+
import getpass
|
|
4
|
+
|
|
5
|
+
# import sys
|
|
6
|
+
#
|
|
7
|
+
#
|
|
8
|
+
# def parse_arguments():
|
|
9
|
+
# args = sys.argv[1:] # Игнорировать первый элемент, так как он является именем скрипта
|
|
10
|
+
# parsed_args = {}
|
|
11
|
+
#
|
|
12
|
+
# i = 0
|
|
13
|
+
# while i < len(args):
|
|
14
|
+
# arg = args[i]
|
|
15
|
+
# if arg.startswith("--"):
|
|
16
|
+
# # Длинный аргумент с префиксом "--"
|
|
17
|
+
# if "=" in arg: # Аргумент содержит значение
|
|
18
|
+
# key, value = arg[2:].split("=")
|
|
19
|
+
# parsed_args[key] = value
|
|
20
|
+
# else: # Аргумент является флагом
|
|
21
|
+
# key = arg[2:]
|
|
22
|
+
# parsed_args[key] = True
|
|
23
|
+
# elif arg.startswith("-"):
|
|
24
|
+
# # Короткая версия аргумента с префиксом "-"
|
|
25
|
+
# if len(arg) > 2: # Аргумент содержит значение
|
|
26
|
+
# key = arg[1]
|
|
27
|
+
# value = arg[2:]
|
|
28
|
+
# parsed_args[key] = value
|
|
29
|
+
# else: # Аргумент является флагом
|
|
30
|
+
# key = arg[1]
|
|
31
|
+
# parsed_args[key] = True
|
|
32
|
+
# i += 1
|
|
33
|
+
#
|
|
34
|
+
# return parsed_args
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def argsparse(arguments, args):
|
|
38
|
+
"""
|
|
39
|
+
@TODO переписать логику
|
|
40
|
+
Парсер аргументов командной строки
|
|
41
|
+
|
|
42
|
+
:param arguments:
|
|
43
|
+
:param args:
|
|
44
|
+
:return:
|
|
45
|
+
"""
|
|
46
|
+
kwargs = {}
|
|
47
|
+
args = list(args)
|
|
48
|
+
i = 0
|
|
49
|
+
arguments.reverse()
|
|
50
|
+
for argument in arguments:
|
|
51
|
+
if argument.required and argument.argument not in args:
|
|
52
|
+
args.insert(i, argument.argument)
|
|
53
|
+
i = i + 1
|
|
54
|
+
new_args = args
|
|
55
|
+
for argument in arguments:
|
|
56
|
+
if argument.nargs > 1 or argument.multiple:
|
|
57
|
+
kwargs[argument.dest] = []
|
|
58
|
+
else:
|
|
59
|
+
kwargs[argument.dest] = argument.default
|
|
60
|
+
_args = iter(args)
|
|
61
|
+
arg = next(_args, None)
|
|
62
|
+
while arg:
|
|
63
|
+
next_arg = next(_args, None)
|
|
64
|
+
if arg == argument.argument or arg == argument.short:
|
|
65
|
+
if arg in new_args:
|
|
66
|
+
new_args.remove(arg)
|
|
67
|
+
if argument.nargs <= 0:
|
|
68
|
+
'''Обрабатываем флаги'''
|
|
69
|
+
kwargs[argument.dest] = argument.flag_value
|
|
70
|
+
elif argument.nargs == 1:
|
|
71
|
+
'''Обрабатываем элемент со значением'''
|
|
72
|
+
value = None if isinstance(next_arg, str) and next_arg[0:1] == '-' else next_arg
|
|
73
|
+
if value:
|
|
74
|
+
if value in new_args:
|
|
75
|
+
new_args.remove(value)
|
|
76
|
+
if argument.multiple:
|
|
77
|
+
kwargs[argument.dest].append(value)
|
|
78
|
+
else:
|
|
79
|
+
kwargs[argument.dest] = value
|
|
80
|
+
if argument.prompt and not kwargs[argument.dest] and argument.password:
|
|
81
|
+
kwargs[argument.dest] = getpass.getpass(
|
|
82
|
+
f"{argument.prompt if argument.prompt else 'Password'}: ").strip()
|
|
83
|
+
if argument.prompt and not kwargs[argument.dest] and not argument.password:
|
|
84
|
+
kwargs[argument.dest] = input(f"{argument.prompt}: ").strip()
|
|
85
|
+
elif argument.nargs > 1:
|
|
86
|
+
'''Обрабатываем списки аргументов'''
|
|
87
|
+
for l in range(argument.nargs):
|
|
88
|
+
value = None if isinstance(next_arg, str) and next_arg[0:1] == '-' else next_arg
|
|
89
|
+
if value:
|
|
90
|
+
if value in new_args:
|
|
91
|
+
new_args.remove(value)
|
|
92
|
+
kwargs[argument.dest].append(value)
|
|
93
|
+
if argument.prompt and len(kwargs[argument.dest]) <= l:
|
|
94
|
+
kwargs[argument.dest].append(input(f"{argument.prompt}: ").strip())
|
|
95
|
+
# arg = next_arg
|
|
96
|
+
if value:
|
|
97
|
+
'''Берем следующий только если текущий был найден,
|
|
98
|
+
иначе будем получать пропуски элементов'''
|
|
99
|
+
next_arg = next(_args, None)
|
|
100
|
+
arg = next_arg
|
|
101
|
+
|
|
102
|
+
if argument.nargs > 1:
|
|
103
|
+
if argument.required and argument.nargs != len(kwargs[argument.dest]):
|
|
104
|
+
raise Exception(
|
|
105
|
+
f"Argument {argument.argument if argument.argument else argument.short} is required")
|
|
106
|
+
elif argument.required and not kwargs[argument.dest]:
|
|
107
|
+
raise Exception(
|
|
108
|
+
f"Argument {argument.argument if argument.argument else argument.short} is required")
|
|
109
|
+
|
|
110
|
+
return tuple(new_args), kwargs
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_arguments(argument_list, args):
|
|
114
|
+
"""
|
|
115
|
+
Парсер аргументов командной строки
|
|
116
|
+
|
|
117
|
+
:param argument_list: Список возможных аргументов командной строки в виде объектов
|
|
118
|
+
:param args: Список аргументов командной строки
|
|
119
|
+
:return: Кортеж с невостребованными аргументами и словарь полученных аргументов
|
|
120
|
+
"""
|
|
121
|
+
kwargs = {}
|
|
122
|
+
unused_args = list(args)
|
|
123
|
+
|
|
124
|
+
for argument in argument_list:
|
|
125
|
+
if argument.argument:
|
|
126
|
+
prefix = f"{argument.argument}="
|
|
127
|
+
for idx, token in enumerate(list(unused_args)):
|
|
128
|
+
if isinstance(token, str) and token.startswith(prefix):
|
|
129
|
+
value = token[len(prefix):]
|
|
130
|
+
unused_args[idx:idx + 1] = [argument.argument, value]
|
|
131
|
+
break
|
|
132
|
+
if argument.short:
|
|
133
|
+
prefix = f"{argument.short}="
|
|
134
|
+
for idx, token in enumerate(list(unused_args)):
|
|
135
|
+
if isinstance(token, str) and token.startswith(prefix):
|
|
136
|
+
value = token[len(prefix):]
|
|
137
|
+
unused_args[idx:idx + 1] = [argument.short, value]
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if argument.dest not in kwargs and argument.default is not None:
|
|
141
|
+
kwargs[argument.dest] = argument.default
|
|
142
|
+
|
|
143
|
+
if argument.short and argument.short in unused_args:
|
|
144
|
+
index = unused_args.index(argument.short)
|
|
145
|
+
process_argument(argument, unused_args, kwargs, index)
|
|
146
|
+
|
|
147
|
+
elif argument.argument and argument.argument in unused_args:
|
|
148
|
+
index = unused_args.index(argument.argument)
|
|
149
|
+
process_argument(argument, unused_args, kwargs, index)
|
|
150
|
+
|
|
151
|
+
if argument.required and argument.dest not in kwargs:
|
|
152
|
+
if argument.prompt:
|
|
153
|
+
value = getpass.getpass(argument.prompt) if argument.hide else input(argument.prompt).strip()
|
|
154
|
+
kwargs[argument.dest] = value.strip()
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Required argument {argument.argument if argument.argument else argument.short} is missing")
|
|
158
|
+
|
|
159
|
+
return tuple(unused_args), kwargs
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def process_argument(argument, unused_args, kwargs, index):
|
|
163
|
+
"""
|
|
164
|
+
Обработка аргумента командной строки
|
|
165
|
+
|
|
166
|
+
:param argument: Объект аргумента командной строки
|
|
167
|
+
:param unused_args: Список невостребованных аргументов
|
|
168
|
+
:param kwargs: Словарь полученных аргументов
|
|
169
|
+
:param index: Индекс аргумента в списке аргументов командной строки
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
if argument.multiple:
|
|
173
|
+
values = []
|
|
174
|
+
for value in unused_args[index + 1:]:
|
|
175
|
+
if value.startswith('-'):
|
|
176
|
+
break
|
|
177
|
+
values.append(value)
|
|
178
|
+
kwargs[argument.dest] = values
|
|
179
|
+
del unused_args[index: index + len(values) + 1]
|
|
180
|
+
|
|
181
|
+
elif argument.nargs > 1:
|
|
182
|
+
values = unused_args[index + 1: index + 1 + argument.nargs]
|
|
183
|
+
kwargs[argument.dest] = values
|
|
184
|
+
del unused_args[index: index + 1 + argument.nargs]
|
|
185
|
+
|
|
186
|
+
elif argument.flag_value is True:
|
|
187
|
+
kwargs[argument.dest] = argument.flag_value
|
|
188
|
+
del unused_args[index]
|
|
189
|
+
|
|
190
|
+
elif argument.nargs == 1:
|
|
191
|
+
try:
|
|
192
|
+
value = unused_args[index + 1].strip()
|
|
193
|
+
kwargs[argument.dest] = value
|
|
194
|
+
del unused_args[index: index + 2]
|
|
195
|
+
except IndexError as e:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
raise ValueError(f"Invalid argument: {unused_args[index]}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def daemon(*args, function=None, as_daemon=False, **kwargs):
|
|
203
|
+
"""
|
|
204
|
+
Создает процесс демона
|
|
205
|
+
|
|
206
|
+
:param args:
|
|
207
|
+
:param function: Функция для запуска в качестве демона
|
|
208
|
+
:param as_daemon: запускает как демон. Т.е. ждем когда функция закончит выполнения перед остановкой демона
|
|
209
|
+
:param kwargs:
|
|
210
|
+
:return:
|
|
211
|
+
"""
|
|
212
|
+
thread = threading.current_thread()
|
|
213
|
+
if callable(function):
|
|
214
|
+
function(thread)
|
|
215
|
+
|
|
216
|
+
# print(thread.__dict__)
|
|
217
|
+
# print(thread.daemon)
|
|
218
|
+
# print(thread.isDaemon())
|
|
219
|
+
# set thread as Daemon
|
|
220
|
+
# thread.setDaemon(True)
|
|
221
|
+
# print(thread.isDaemon())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from .command import Group, Command
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Console:
|
|
9
|
+
"""
|
|
10
|
+
Главный инстанс для подключения консольных команды
|
|
11
|
+
Так же создает базовую консольную группу cli
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
size = shutil.get_terminal_size(fallback=(100, 50))
|
|
17
|
+
rows, columns = size.lines, size.columns
|
|
18
|
+
except BaseException as e:
|
|
19
|
+
rows = 50
|
|
20
|
+
columns = 100
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.root_group = Group(['cli'])
|
|
24
|
+
self.root_group.command_name = 'cli'
|
|
25
|
+
self.root_group.handler = self.handler
|
|
26
|
+
self.root_group.description = ''
|
|
27
|
+
self.root_group.function_name = 'console.handler'
|
|
28
|
+
|
|
29
|
+
command = Command(['help'] + self.root_group.key)
|
|
30
|
+
command.function_name = self.root_group.help.__name__
|
|
31
|
+
command.handler = self.root_group.help
|
|
32
|
+
command.command_name = 'help'
|
|
33
|
+
command.description = "Command List"
|
|
34
|
+
|
|
35
|
+
self.root_group.add(command)
|
|
36
|
+
|
|
37
|
+
def __call__(self, *args, **kwargs):
|
|
38
|
+
return self.root_group
|
|
39
|
+
|
|
40
|
+
def handler(self, *args, **kwargs):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
console = Console()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
cli = console.root_group
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from muscles.core import BaseStrategy
|
|
2
|
+
import io
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import shlex
|
|
6
|
+
from typing import Any, Optional, cast
|
|
7
|
+
from .colory import Colors
|
|
8
|
+
from .instance import cli, Console
|
|
9
|
+
from .error_handler import ConsoleErrorHandler
|
|
10
|
+
from .constants import CLI_HEADER_AUTHOR, CLI_HEADER_SUBTITLE, CLI_HEADER_TITLE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class flushfile(object):
|
|
14
|
+
|
|
15
|
+
def __init__(self, f):
|
|
16
|
+
self.f = f
|
|
17
|
+
|
|
18
|
+
def write(self, x):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def flush(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CliStrategy(BaseStrategy):
|
|
26
|
+
"""
|
|
27
|
+
Стратегия для контекста переводящая обработку в режим работы в консоли
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
size = shutil.get_terminal_size(fallback=(100, 50))
|
|
32
|
+
rows, columns = size.lines, size.columns
|
|
33
|
+
except:
|
|
34
|
+
rows = 50
|
|
35
|
+
columns = 100
|
|
36
|
+
|
|
37
|
+
def _print_header(self):
|
|
38
|
+
"""
|
|
39
|
+
Печатаем базовый заголовок в консоли
|
|
40
|
+
|
|
41
|
+
:return:
|
|
42
|
+
"""
|
|
43
|
+
author = getattr(self, "cli_author", CLI_HEADER_AUTHOR)
|
|
44
|
+
title = getattr(self, "cli_title", CLI_HEADER_TITLE)
|
|
45
|
+
subtitle = getattr(self, "cli_subtitle", CLI_HEADER_SUBTITLE)
|
|
46
|
+
lines = [
|
|
47
|
+
'-' * (int(self.columns) - 4),
|
|
48
|
+
title,
|
|
49
|
+
subtitle,
|
|
50
|
+
author.rjust(int(self.columns)-len(author)),
|
|
51
|
+
'-' * (int(self.columns) - 4),
|
|
52
|
+
]
|
|
53
|
+
for line in lines:
|
|
54
|
+
print(f"{Colors.HEADER}--{line.center(int(self.columns)-4)}--{Colors.ENDC}")
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _resolve_root_group(context, kwargs) -> object | None:
|
|
58
|
+
root_group = kwargs.get('root_group')
|
|
59
|
+
if root_group is not None:
|
|
60
|
+
return root_group
|
|
61
|
+
|
|
62
|
+
if hasattr(context, 'param'):
|
|
63
|
+
try:
|
|
64
|
+
return context.param('root_group')
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from .instance import cli as default_root_group
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
return default_root_group
|
|
73
|
+
|
|
74
|
+
def execute(self, *args,
|
|
75
|
+
error_handler: Optional[ConsoleErrorHandler] = None,
|
|
76
|
+
shutup=False,
|
|
77
|
+
print_header=False,
|
|
78
|
+
**kwargs) -> Any:
|
|
79
|
+
"""
|
|
80
|
+
Первичный обработчик консольных команд
|
|
81
|
+
|
|
82
|
+
:param args:
|
|
83
|
+
:param shutup: приглушаем весь вывод в stdout
|
|
84
|
+
:param error_handler:
|
|
85
|
+
:param kwargs:
|
|
86
|
+
:return:
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
self.cli_title = kwargs.get("title", CLI_HEADER_TITLE)
|
|
90
|
+
self.cli_subtitle = kwargs.get("subtitle", CLI_HEADER_SUBTITLE)
|
|
91
|
+
self.cli_author = kwargs.get("author", CLI_HEADER_AUTHOR)
|
|
92
|
+
container = kwargs.get('container')
|
|
93
|
+
if shutup:
|
|
94
|
+
sys.stdout = io.StringIO()
|
|
95
|
+
print_header = False
|
|
96
|
+
if not print_header:
|
|
97
|
+
self._print_header()
|
|
98
|
+
if len(args) <= 0:
|
|
99
|
+
args = sys.argv[1:]
|
|
100
|
+
elif len(args) == 1 and isinstance(args[0], str):
|
|
101
|
+
args = tuple(shlex.split(args[0]))
|
|
102
|
+
root_group = self._resolve_root_group(container, kwargs)
|
|
103
|
+
if root_group is None:
|
|
104
|
+
raise RuntimeError("CLI root group is not configured")
|
|
105
|
+
result = cast(Any, root_group).execute(*args)
|
|
106
|
+
if shutup:
|
|
107
|
+
sys.stdout = sys.__stdout__
|
|
108
|
+
return result
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
print(f"\n\n{Colors.WARNING}The programme was terminated (Ctrl+C).{Colors.ENDC}")
|
|
111
|
+
return None
|
muscles/cli/providers.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from muscles.core import GenerationRequest, GeneratorRegistry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _write(path: Path, content: str) -> None:
|
|
9
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
path.write_text(content, encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _safe_write_file(path: Path, content: str, force: bool) -> str:
|
|
14
|
+
if path.exists() and not force:
|
|
15
|
+
raise FileExistsError(f"File `{path}` already exists. Use --force to overwrite.")
|
|
16
|
+
_write(path, content)
|
|
17
|
+
return str(path)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CliCommandGeneratorProvider:
|
|
21
|
+
name = "cli-command"
|
|
22
|
+
|
|
23
|
+
def supports(self, generator_type: str) -> bool:
|
|
24
|
+
return generator_type == "cli-command"
|
|
25
|
+
|
|
26
|
+
def generate(self, project_root: Path, request: GenerationRequest) -> list[str]:
|
|
27
|
+
app_dir = project_root / "app"
|
|
28
|
+
slug = request.name.replace(".", "_").replace("-", "_").lower()
|
|
29
|
+
generated = [
|
|
30
|
+
str(
|
|
31
|
+
Path(
|
|
32
|
+
_safe_write_file(
|
|
33
|
+
app_dir / "cli" / f"{slug}.py",
|
|
34
|
+
f"def {slug}(*args, **kwargs):\n return True\n",
|
|
35
|
+
request.force,
|
|
36
|
+
)
|
|
37
|
+
).relative_to(project_root)
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
if request.with_tests:
|
|
41
|
+
generated.append(
|
|
42
|
+
str(
|
|
43
|
+
Path(
|
|
44
|
+
_safe_write_file(
|
|
45
|
+
project_root / "tests" / f"test_cli_{slug}.py",
|
|
46
|
+
f"def test_cli_{slug}():\n assert True\n",
|
|
47
|
+
request.force,
|
|
48
|
+
)
|
|
49
|
+
).relative_to(project_root)
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
return generated
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CoreGeneratorProvider:
|
|
56
|
+
name = "core"
|
|
57
|
+
|
|
58
|
+
def supports(self, generator_type: str) -> bool:
|
|
59
|
+
return generator_type in {"page", "api-resource", "value-object", "resource"}
|
|
60
|
+
|
|
61
|
+
def generate(self, project_root: Path, request: GenerationRequest) -> list[str]:
|
|
62
|
+
app_dir = project_root / "app"
|
|
63
|
+
slug = request.name.replace(".", "_").replace("-", "_").lower()
|
|
64
|
+
generated: list[str] = []
|
|
65
|
+
generator_type = request.generator_type
|
|
66
|
+
|
|
67
|
+
if generator_type == "page":
|
|
68
|
+
generated.append(
|
|
69
|
+
str(
|
|
70
|
+
Path(
|
|
71
|
+
_safe_write_file(
|
|
72
|
+
app_dir / "web" / f"{slug}.py",
|
|
73
|
+
f"def {slug}(*args, **kwargs):\n return '{request.name}'\n",
|
|
74
|
+
request.force,
|
|
75
|
+
)
|
|
76
|
+
).relative_to(project_root)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
if request.with_tests:
|
|
80
|
+
generated.append(
|
|
81
|
+
str(
|
|
82
|
+
Path(
|
|
83
|
+
_safe_write_file(
|
|
84
|
+
project_root / "tests" / f"test_page_{slug}.py",
|
|
85
|
+
f"def test_page_{slug}():\n assert True\n",
|
|
86
|
+
request.force,
|
|
87
|
+
)
|
|
88
|
+
).relative_to(project_root)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
return generated
|
|
92
|
+
|
|
93
|
+
if generator_type == "api-resource":
|
|
94
|
+
generated.append(
|
|
95
|
+
str(
|
|
96
|
+
Path(
|
|
97
|
+
_safe_write_file(
|
|
98
|
+
app_dir / "api" / f"{slug}.py",
|
|
99
|
+
f"def create_{slug}(*args, **kwargs):\n return {{'resource': '{request.name}'}}\n",
|
|
100
|
+
request.force,
|
|
101
|
+
)
|
|
102
|
+
).relative_to(project_root)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
generated.append(
|
|
106
|
+
str(
|
|
107
|
+
Path(
|
|
108
|
+
_safe_write_file(
|
|
109
|
+
app_dir / "schemas" / f"{slug}.py",
|
|
110
|
+
f"class {request.name}Schema:\n pass\n",
|
|
111
|
+
request.force,
|
|
112
|
+
)
|
|
113
|
+
).relative_to(project_root)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if request.with_tests:
|
|
117
|
+
generated.append(
|
|
118
|
+
str(
|
|
119
|
+
Path(
|
|
120
|
+
_safe_write_file(
|
|
121
|
+
project_root / "tests" / f"test_api_{slug}.py",
|
|
122
|
+
f"def test_api_{slug}():\n assert True\n",
|
|
123
|
+
request.force,
|
|
124
|
+
)
|
|
125
|
+
).relative_to(project_root)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return generated
|
|
129
|
+
|
|
130
|
+
if generator_type == "value-object":
|
|
131
|
+
generated.append(
|
|
132
|
+
str(
|
|
133
|
+
Path(
|
|
134
|
+
_safe_write_file(
|
|
135
|
+
app_dir / "domain" / f"{slug}.py",
|
|
136
|
+
f"class {request.name}:\n pass\n",
|
|
137
|
+
request.force,
|
|
138
|
+
)
|
|
139
|
+
).relative_to(project_root)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
if request.with_tests:
|
|
143
|
+
generated.append(
|
|
144
|
+
str(
|
|
145
|
+
Path(
|
|
146
|
+
_safe_write_file(
|
|
147
|
+
project_root / "tests" / f"test_value_object_{slug}.py",
|
|
148
|
+
f"def test_value_object_{slug}():\n assert True\n",
|
|
149
|
+
request.force,
|
|
150
|
+
)
|
|
151
|
+
).relative_to(project_root)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return generated
|
|
155
|
+
|
|
156
|
+
if generator_type == "resource":
|
|
157
|
+
registry = default_generator_registry()
|
|
158
|
+
generated.extend(
|
|
159
|
+
registry.resolve("api-resource").generate(
|
|
160
|
+
project_root,
|
|
161
|
+
GenerationRequest(generator_type="api-resource", name=request.name, force=request.force, with_tests=False),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
generated.extend(
|
|
165
|
+
registry.resolve("cli-command").generate(
|
|
166
|
+
project_root,
|
|
167
|
+
GenerationRequest(generator_type="cli-command", name=request.name, force=request.force, with_tests=False),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
if request.with_tests:
|
|
171
|
+
generated.append(
|
|
172
|
+
str(
|
|
173
|
+
Path(
|
|
174
|
+
_safe_write_file(
|
|
175
|
+
project_root / "tests" / f"test_resource_{slug}.py",
|
|
176
|
+
f"def test_resource_{slug}():\n assert True\n",
|
|
177
|
+
request.force,
|
|
178
|
+
)
|
|
179
|
+
).relative_to(project_root)
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return generated
|
|
183
|
+
|
|
184
|
+
raise ValueError(f"Unsupported generator type `{generator_type}`")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
_DEFAULT_REGISTRY: GeneratorRegistry | None = None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def default_generator_registry() -> GeneratorRegistry:
|
|
191
|
+
global _DEFAULT_REGISTRY
|
|
192
|
+
if _DEFAULT_REGISTRY is None:
|
|
193
|
+
registry = GeneratorRegistry()
|
|
194
|
+
registry.register(CoreGeneratorProvider())
|
|
195
|
+
registry.register(CliCommandGeneratorProvider())
|
|
196
|
+
_DEFAULT_REGISTRY = registry
|
|
197
|
+
return _DEFAULT_REGISTRY
|
|
198
|
+
|
muscles/cli/streaming.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, TextIO
|
|
7
|
+
|
|
8
|
+
from muscles.core import stream_events
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class CliStreamRenderResult:
|
|
13
|
+
exit_code: int
|
|
14
|
+
events: list[dict[str, Any]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_stream_result(
|
|
18
|
+
stream_result: Any,
|
|
19
|
+
*,
|
|
20
|
+
json_lines: bool = False,
|
|
21
|
+
stdout: TextIO | None = None,
|
|
22
|
+
stderr: TextIO | None = None,
|
|
23
|
+
) -> CliStreamRenderResult:
|
|
24
|
+
out = stdout or sys.stdout
|
|
25
|
+
err = stderr or sys.stderr
|
|
26
|
+
rendered: list[dict[str, Any]] = []
|
|
27
|
+
exit_code = 0
|
|
28
|
+
|
|
29
|
+
for event in stream_events(stream_result):
|
|
30
|
+
payload = {
|
|
31
|
+
"event": event.type,
|
|
32
|
+
"data": event.data,
|
|
33
|
+
"id": event.event_id,
|
|
34
|
+
"metadata": dict(event.metadata),
|
|
35
|
+
}
|
|
36
|
+
rendered.append(payload)
|
|
37
|
+
if json_lines:
|
|
38
|
+
out.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
39
|
+
continue
|
|
40
|
+
if event.type == "progress":
|
|
41
|
+
out.write(f"progress: {event.data}\n")
|
|
42
|
+
elif event.type == "log":
|
|
43
|
+
out.write(f"log: {event.data}\n")
|
|
44
|
+
elif event.type == "result":
|
|
45
|
+
out.write(f"result: {event.data}\n")
|
|
46
|
+
elif event.type == "error":
|
|
47
|
+
exit_code = 1
|
|
48
|
+
err.write(f"error: {event.data}\n")
|
|
49
|
+
|
|
50
|
+
return CliStreamRenderResult(exit_code=exit_code, events=rendered)
|