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.
@@ -0,0 +1,3 @@
1
+ CLI_HEADER_TITLE = "Muscular"
2
+ CLI_HEADER_SUBTITLE = "CLI"
3
+ CLI_HEADER_AUTHOR = "(c) Muscles Team"
@@ -0,0 +1,6 @@
1
+
2
+
3
+ class ConsoleErrorHandler(Exception):
4
+
5
+ def handler(self, reason: str | None = None):
6
+ return Exception(reason)
@@ -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
@@ -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
+
@@ -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)