jazzoo 0.1.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.
- jazzoo/__init__.py +57 -0
- jazzoo/aliases/__init__.py +0 -0
- jazzoo/aliases/errors.py +5 -0
- jazzoo/aliases/rules/__init__.py +0 -0
- jazzoo/aliases/rules/expand_alias.py +38 -0
- jazzoo/aliases/rules/get_alias_config_file.py +7 -0
- jazzoo/aliases/rules/read_aliases.py +36 -0
- jazzoo/blocks/__init__.py +0 -0
- jazzoo/blocks/actions/__init__.py +0 -0
- jazzoo/blocks/actions/run_block.py +80 -0
- jazzoo/blocks/models/__init__.py +0 -0
- jazzoo/blocks/models/block.py +21 -0
- jazzoo/blocks/rules/__init__.py +0 -0
- jazzoo/commands/__init__.py +19 -0
- jazzoo/commands/__main__.py +5 -0
- jazzoo/commands/actions/__init__.py +0 -0
- jazzoo/commands/actions/confirm_command.py +36 -0
- jazzoo/commands/actions/run.py +100 -0
- jazzoo/commands/actions/run_docker.py +67 -0
- jazzoo/commands/entrypoints/__init__.py +0 -0
- jazzoo/commands/entrypoints/jaz_run.py +76 -0
- jazzoo/commands/errors.py +26 -0
- jazzoo/commands/models/__init__.py +0 -0
- jazzoo/commands/models/command_result.py +15 -0
- jazzoo/commands/rules/__init__.py +0 -0
- jazzoo/commands/rules/find_executable.py +18 -0
- jazzoo/commands/rules/has_terminal.py +9 -0
- jazzoo/commands/rules/is_confirm_on.py +7 -0
- jazzoo/commands/rules/needs_tty.py +9 -0
- jazzoo/commands/settings.py +7 -0
- jazzoo/commands/utils/__init__.py +0 -0
- jazzoo/commands/utils/dataclass.py +34 -0
- jazzoo/commands/utils/format_command.py +9 -0
- jazzoo/commands/utils/ignoring_sigint.py +18 -0
- jazzoo/config/__init__.py +17 -0
- jazzoo/config/_vendor/__init__.py +0 -0
- jazzoo/config/_vendor/vendor.txt +1 -0
- jazzoo/config/_vendor/yaml/LICENSE +20 -0
- jazzoo/config/_vendor/yaml/__init__.py +390 -0
- jazzoo/config/_vendor/yaml/composer.py +139 -0
- jazzoo/config/_vendor/yaml/constructor.py +748 -0
- jazzoo/config/_vendor/yaml/dumper.py +62 -0
- jazzoo/config/_vendor/yaml/emitter.py +1137 -0
- jazzoo/config/_vendor/yaml/error.py +75 -0
- jazzoo/config/_vendor/yaml/events.py +86 -0
- jazzoo/config/_vendor/yaml/loader.py +63 -0
- jazzoo/config/_vendor/yaml/nodes.py +49 -0
- jazzoo/config/_vendor/yaml/parser.py +589 -0
- jazzoo/config/_vendor/yaml/reader.py +185 -0
- jazzoo/config/_vendor/yaml/representer.py +389 -0
- jazzoo/config/_vendor/yaml/resolver.py +227 -0
- jazzoo/config/_vendor/yaml/scanner.py +1435 -0
- jazzoo/config/_vendor/yaml/serializer.py +111 -0
- jazzoo/config/_vendor/yaml/tokens.py +104 -0
- jazzoo/config/errors.py +14 -0
- jazzoo/config/models/__init__.py +0 -0
- jazzoo/config/models/loaded_config.py +17 -0
- jazzoo/config/rules/__init__.py +0 -0
- jazzoo/config/rules/expand_include_path.py +73 -0
- jazzoo/config/rules/get_config.py +26 -0
- jazzoo/config/rules/get_loaded_config.py +63 -0
- jazzoo/config/rules/get_value.py +199 -0
- jazzoo/config/rules/prepare_config.py +5 -0
- jazzoo/config/rules/read_config_file.py +30 -0
- jazzoo/config/rules/read_config_layers.py +58 -0
- jazzoo/config/rules/split_config_path.py +7 -0
- jazzoo/config/settings.py +6 -0
- jazzoo/config/utils/__init__.py +0 -0
- jazzoo/config/utils/dataclass.py +34 -0
- jazzoo/config/utils/format_config_path.py +5 -0
- jazzoo/config/utils/merge_config.py +11 -0
- jazzoo/config/utils/render_scalar.py +17 -0
- jazzoo/config/utils/split_expressions.py +59 -0
- jazzoo/enums.py +15 -0
- jazzoo/environment/__init__.py +0 -0
- jazzoo/environment/actions/__init__.py +0 -0
- jazzoo/environment/actions/load_config.py +26 -0
- jazzoo/environment/actions/load_env.py +30 -0
- jazzoo/environment/utils/__init__.py +0 -0
- jazzoo/environment/utils/parse_dotenv.py +17 -0
- jazzoo/errors.py +43 -0
- jazzoo/expressions/__init__.py +0 -0
- jazzoo/expressions/actions/__init__.py +0 -0
- jazzoo/expressions/actions/evaluate.py +17 -0
- jazzoo/expressions/actions/evaluate_expression.py +344 -0
- jazzoo/expressions/models/__init__.py +0 -0
- jazzoo/expressions/models/boolean.py +25 -0
- jazzoo/expressions/models/literal.py +11 -0
- jazzoo/expressions/models/path_join.py +12 -0
- jazzoo/expressions/models/pipe.py +13 -0
- jazzoo/expressions/models/reference.py +21 -0
- jazzoo/expressions/models/ternary.py +20 -0
- jazzoo/expressions/rules/__init__.py +0 -0
- jazzoo/expressions/rules/apply_builtin_pipe.py +55 -0
- jazzoo/expressions/rules/get_by_path.py +47 -0
- jazzoo/expressions/rules/is_truthy.py +22 -0
- jazzoo/expressions/rules/parse_expression.py +201 -0
- jazzoo/expressions/rules/parse_references.py +318 -0
- jazzoo/expressions/rules/resolve_reference.py +71 -0
- jazzoo/expressions/rules/split_reference_path.py +25 -0
- jazzoo/expressions/rules/substitute_references.py +51 -0
- jazzoo/expressions/rules/tokenize_expression.py +207 -0
- jazzoo/expressions/utils/__init__.py +0 -0
- jazzoo/expressions/utils/join_path.py +13 -0
- jazzoo/expressions/utils/render_value.py +22 -0
- jazzoo/expressions/utils/shell_quote.py +6 -0
- jazzoo/expressions/utils/strip_one_newline.py +5 -0
- jazzoo/functions/__init__.py +0 -0
- jazzoo/functions/actions/__init__.py +0 -0
- jazzoo/functions/actions/call_builtin_function.py +62 -0
- jazzoo/functions/actions/call_function.py +262 -0
- jazzoo/functions/models/__init__.py +0 -0
- jazzoo/functions/models/call_site.py +25 -0
- jazzoo/functions/models/curried_function.py +18 -0
- jazzoo/functions/models/function.py +32 -0
- jazzoo/functions/models/parameter.py +11 -0
- jazzoo/functions/rules/__init__.py +0 -0
- jazzoo/functions/rules/format_signature.py +18 -0
- jazzoo/functions/rules/get_function_by_path.py +25 -0
- jazzoo/functions/rules/get_target_by_path.py +41 -0
- jazzoo/functions/rules/get_targets.py +13 -0
- jazzoo/functions/rules/parse_curry.py +81 -0
- jazzoo/modules/__init__.py +0 -0
- jazzoo/modules/actions/__init__.py +0 -0
- jazzoo/modules/actions/parse_module.py +248 -0
- jazzoo/modules/actions/resolve_import.py +261 -0
- jazzoo/modules/models/__init__.py +0 -0
- jazzoo/modules/models/absent.py +14 -0
- jazzoo/modules/models/assignment.py +13 -0
- jazzoo/modules/models/builtin_call.py +14 -0
- jazzoo/modules/models/frame.py +86 -0
- jazzoo/modules/models/import_statement.py +26 -0
- jazzoo/modules/models/module.py +28 -0
- jazzoo/modules/models/parsed_source.py +15 -0
- jazzoo/modules/models/scope.py +13 -0
- jazzoo/modules/models/variable.py +13 -0
- jazzoo/modules/rules/__init__.py +0 -0
- jazzoo/modules/rules/get_module_by_path.py +22 -0
- jazzoo/modules/rules/list_targets.py +46 -0
- jazzoo/modules/rules/parse_attribute_line.py +31 -0
- jazzoo/modules/rules/parse_body.py +136 -0
- jazzoo/modules/rules/parse_call_arguments.py +19 -0
- jazzoo/modules/rules/parse_dependencies.py +20 -0
- jazzoo/modules/rules/parse_import_statement.py +101 -0
- jazzoo/modules/rules/parse_parameters.py +18 -0
- jazzoo/modules/rules/parse_source.py +154 -0
- jazzoo/modules/rules/validate_module.py +489 -0
- jazzoo/runtime/__init__.py +0 -0
- jazzoo/runtime/entrypoints/__init__.py +0 -0
- jazzoo/runtime/entrypoints/jazzoo.py +214 -0
- jazzoo/runtime/entrypoints/zoo.py +150 -0
- jazzoo/runtime/utils/__init__.py +0 -0
- jazzoo/runtime/utils/format_error.py +13 -0
- jazzoo/runtime/utils/test_utils.py +103 -0
- jazzoo/settings.py +40 -0
- jazzoo/utils/__init__.py +0 -0
- jazzoo/utils/dataclass.py +34 -0
- jazzoo/utils/indent_of.py +2 -0
- jazzoo/utils/join_continuations.py +25 -0
- jazzoo/utils/split_top_level.py +31 -0
- jazzoo/zoo/__init__.py +0 -0
- jazzoo/zoo/actions/__init__.py +0 -0
- jazzoo/zoo/actions/activate_zoo_env.py +16 -0
- jazzoo/zoo/actions/create_venv.py +148 -0
- jazzoo/zoo/actions/forget_zoo_env.py +31 -0
- jazzoo/zoo/actions/init_zoo_env.py +129 -0
- jazzoo/zoo/actions/install_fish_files.py +84 -0
- jazzoo/zoo/actions/run_in_zoo_env.py +29 -0
- jazzoo/zoo/actions/write_latest_zoo_env_scripts.py +21 -0
- jazzoo/zoo/actions/write_recent_zoo_envs.py +19 -0
- jazzoo/zoo/errors.py +61 -0
- jazzoo/zoo/models/__init__.py +0 -0
- jazzoo/zoo/models/zoo_env.py +9 -0
- jazzoo/zoo/rules/__init__.py +0 -0
- jazzoo/zoo/rules/find_zoo_env_of_venv.py +14 -0
- jazzoo/zoo/rules/format_zoo_env_list.py +18 -0
- jazzoo/zoo/rules/get_activate_filename.py +8 -0
- jazzoo/zoo/rules/get_fish_config_dir.py +8 -0
- jazzoo/zoo/rules/get_jazzoo_version.py +30 -0
- jazzoo/zoo/rules/get_recent_zoo_env.py +10 -0
- jazzoo/zoo/rules/get_shell_name.py +8 -0
- jazzoo/zoo/rules/get_zoo_env.py +17 -0
- jazzoo/zoo/rules/get_zoo_env_dir.py +7 -0
- jazzoo/zoo/rules/get_zoo_link.py +7 -0
- jazzoo/zoo/rules/get_zoo_venv_dir.py +6 -0
- jazzoo/zoo/rules/list_zoo_envs.py +17 -0
- jazzoo/zoo/rules/read_recent_zoo_envs.py +17 -0
- jazzoo/zoo/rules/validate_zoo_env_name.py +13 -0
- jazzoo-0.1.0.dist-info/METADATA +18 -0
- jazzoo-0.1.0.dist-info/RECORD +194 -0
- jazzoo-0.1.0.dist-info/WHEEL +5 -0
- jazzoo-0.1.0.dist-info/entry_points.txt +5 -0
- jazzoo-0.1.0.dist-info/licenses/LICENSE +21 -0
- jazzoo-0.1.0.dist-info/top_level.txt +1 -0
jazzoo/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from jazzoo.commands import (
|
|
2
|
+
CommandAbortedError,
|
|
3
|
+
CommandError,
|
|
4
|
+
CommandFailedError,
|
|
5
|
+
CommandNotFoundError,
|
|
6
|
+
CommandResult,
|
|
7
|
+
run,
|
|
8
|
+
run_docker,
|
|
9
|
+
)
|
|
10
|
+
from jazzoo.config import (
|
|
11
|
+
ConfigError,
|
|
12
|
+
ConfigExpansionError,
|
|
13
|
+
ConfigFileError,
|
|
14
|
+
ConfigKeyError,
|
|
15
|
+
get_config,
|
|
16
|
+
prepare_config,
|
|
17
|
+
)
|
|
18
|
+
from jazzoo.errors import (
|
|
19
|
+
ArgumentError,
|
|
20
|
+
ExecutionError,
|
|
21
|
+
JazzooError,
|
|
22
|
+
ModuleImportError,
|
|
23
|
+
ParseError,
|
|
24
|
+
ResolutionError,
|
|
25
|
+
)
|
|
26
|
+
from jazzoo.expressions.actions.evaluate import evaluate
|
|
27
|
+
from jazzoo.functions.rules.get_function_by_path import get_function_by_path
|
|
28
|
+
from jazzoo.functions.rules.get_targets import get_targets
|
|
29
|
+
from jazzoo.modules.actions.parse_module import parse_module
|
|
30
|
+
from jazzoo.modules.rules.get_module_by_path import get_module_by_path
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ArgumentError",
|
|
34
|
+
"CommandAbortedError",
|
|
35
|
+
"CommandError",
|
|
36
|
+
"CommandFailedError",
|
|
37
|
+
"CommandNotFoundError",
|
|
38
|
+
"CommandResult",
|
|
39
|
+
"ConfigError",
|
|
40
|
+
"ConfigExpansionError",
|
|
41
|
+
"ConfigFileError",
|
|
42
|
+
"ConfigKeyError",
|
|
43
|
+
"ExecutionError",
|
|
44
|
+
"JazzooError",
|
|
45
|
+
"ModuleImportError",
|
|
46
|
+
"ParseError",
|
|
47
|
+
"ResolutionError",
|
|
48
|
+
"evaluate",
|
|
49
|
+
"get_config",
|
|
50
|
+
"get_function_by_path",
|
|
51
|
+
"get_module_by_path",
|
|
52
|
+
"get_targets",
|
|
53
|
+
"parse_module",
|
|
54
|
+
"prepare_config",
|
|
55
|
+
"run",
|
|
56
|
+
"run_docker",
|
|
57
|
+
]
|
|
File without changes
|
jazzoo/aliases/errors.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import typing as T
|
|
3
|
+
|
|
4
|
+
from jazzoo.aliases.errors import AliasError
|
|
5
|
+
from jazzoo.aliases.rules.get_alias_config_file import get_alias_config_file
|
|
6
|
+
from jazzoo.aliases.rules.read_aliases import read_aliases
|
|
7
|
+
|
|
8
|
+
# The options of jaz that an expansion may contain
|
|
9
|
+
_ALLOWED_OPTIONS = ("--confirm", "--trace")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def expand_alias(name: str) -> T.Optional[T.List[str]]:
|
|
13
|
+
config_file = get_alias_config_file()
|
|
14
|
+
expansion = read_aliases(config_file).get(name)
|
|
15
|
+
if expansion is None:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
tokens = shlex.split(expansion)
|
|
20
|
+
except ValueError as error:
|
|
21
|
+
raise AliasError(
|
|
22
|
+
f'alias "{name}" has an invalid expansion: {error}', filename=config_file
|
|
23
|
+
)
|
|
24
|
+
if not tokens:
|
|
25
|
+
raise AliasError(f'alias "{name}" has an empty expansion', filename=config_file)
|
|
26
|
+
|
|
27
|
+
# Only the options that change how the target runs are allowed
|
|
28
|
+
for token in tokens:
|
|
29
|
+
option = token.partition("=")[0]
|
|
30
|
+
if option.startswith("-") and option not in _ALLOWED_OPTIONS:
|
|
31
|
+
raise AliasError(
|
|
32
|
+
f'alias "{name}" may not use the option {option}',
|
|
33
|
+
filename=config_file,
|
|
34
|
+
)
|
|
35
|
+
if all(token in _ALLOWED_OPTIONS for token in tokens):
|
|
36
|
+
raise AliasError(f'alias "{name}" names no target', filename=config_file)
|
|
37
|
+
|
|
38
|
+
return tokens
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import typing as T
|
|
5
|
+
|
|
6
|
+
from jazzoo.aliases.errors import AliasError
|
|
7
|
+
|
|
8
|
+
# A target path: names separated by dots
|
|
9
|
+
_ALIAS_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_aliases(config_file: str) -> T.Dict[str, str]:
|
|
13
|
+
if not os.path.exists(config_file):
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
# No section is special: a section header cannot contain a newline, so the
|
|
17
|
+
# default section of configparser never occurs
|
|
18
|
+
parser = configparser.ConfigParser(
|
|
19
|
+
interpolation=None, strict=True, default_section="\n"
|
|
20
|
+
)
|
|
21
|
+
# Alias names are case-sensitive
|
|
22
|
+
parser.optionxform = str # type: ignore[assignment]
|
|
23
|
+
try:
|
|
24
|
+
with open(config_file, encoding="utf-8") as f:
|
|
25
|
+
parser.read_file(f, source=config_file)
|
|
26
|
+
except (OSError, UnicodeDecodeError, configparser.Error) as error:
|
|
27
|
+
raise AliasError(f"invalid config file: {error}", filename=config_file)
|
|
28
|
+
|
|
29
|
+
if not parser.has_section("alias"):
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
aliases = dict(parser.items("alias"))
|
|
33
|
+
for name in aliases:
|
|
34
|
+
if not _ALIAS_NAME_RE.match(name):
|
|
35
|
+
raise AliasError(f'invalid alias name "{name}"', filename=config_file)
|
|
36
|
+
return aliases
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import stat
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import typing as T
|
|
7
|
+
|
|
8
|
+
from jazzoo.errors import ExecutionError
|
|
9
|
+
from jazzoo.settings import DEFAULT_SHEBANG
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_block(
|
|
13
|
+
shebang: T.Optional[str],
|
|
14
|
+
body: str,
|
|
15
|
+
filename: T.Optional[str] = None,
|
|
16
|
+
line: T.Optional[int] = None,
|
|
17
|
+
is_quiet: bool = False,
|
|
18
|
+
is_interactive: bool = False,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Run one block as a single subprocess and return its raw stdout.
|
|
21
|
+
|
|
22
|
+
The block is written to a temporary executable file so the shebang selects the
|
|
23
|
+
interpreter, exactly as it would for a script on disk.
|
|
24
|
+
"""
|
|
25
|
+
shebang = shebang or DEFAULT_SHEBANG
|
|
26
|
+
script = shebang + "\n" + body
|
|
27
|
+
if not script.endswith("\n"):
|
|
28
|
+
script += "\n"
|
|
29
|
+
|
|
30
|
+
handle = tempfile.NamedTemporaryFile(
|
|
31
|
+
mode="w", suffix=".jaz-block", delete=False, encoding="utf-8"
|
|
32
|
+
)
|
|
33
|
+
try:
|
|
34
|
+
handle.write(script)
|
|
35
|
+
handle.close()
|
|
36
|
+
os.chmod(handle.name, os.stat(handle.name).st_mode | stat.S_IEXEC)
|
|
37
|
+
|
|
38
|
+
if is_interactive:
|
|
39
|
+
# Whatever was echoed must reach the console before the block's output
|
|
40
|
+
sys.stdout.flush()
|
|
41
|
+
sys.stderr.flush()
|
|
42
|
+
completed = subprocess.run([handle.name])
|
|
43
|
+
else:
|
|
44
|
+
completed = subprocess.run(
|
|
45
|
+
[handle.name],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
finally:
|
|
50
|
+
os.unlink(handle.name)
|
|
51
|
+
|
|
52
|
+
# An interactive block writes to the console itself, and Jazzoo never sees its
|
|
53
|
+
# output, not even in a quiet call.
|
|
54
|
+
if is_interactive:
|
|
55
|
+
if completed.returncode != 0:
|
|
56
|
+
raise ExecutionError(
|
|
57
|
+
f"recipe failed with exit code {completed.returncode}: "
|
|
58
|
+
f"command failed: {body.strip()}",
|
|
59
|
+
filename=filename,
|
|
60
|
+
line=line,
|
|
61
|
+
)
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
# The block's own output goes to the console as well as being captured for
|
|
65
|
+
# ⚙️.stdout, so a target behaves like the commands it runs. A quiet block keeps
|
|
66
|
+
# its stdout for the caller, but its stderr is always shown.
|
|
67
|
+
if completed.stdout and not is_quiet:
|
|
68
|
+
print(completed.stdout, end="")
|
|
69
|
+
if completed.stderr:
|
|
70
|
+
print(completed.stderr, end="", file=sys.stderr)
|
|
71
|
+
|
|
72
|
+
if completed.returncode != 0:
|
|
73
|
+
message = completed.stderr.strip() or f"command failed: {body.strip()}"
|
|
74
|
+
raise ExecutionError(
|
|
75
|
+
f"recipe failed with exit code {completed.returncode}: {message}",
|
|
76
|
+
filename=filename,
|
|
77
|
+
line=line,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return completed.stdout
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import typing as T
|
|
2
|
+
|
|
3
|
+
from jazzoo.enums import BlockMode
|
|
4
|
+
from jazzoo.utils.dataclass import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class BlockLine:
|
|
9
|
+
text: str
|
|
10
|
+
line: int
|
|
11
|
+
# Set by a leading `@`, which hides the echoed command but not its output.
|
|
12
|
+
is_quiet: bool = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Block:
|
|
17
|
+
mode: BlockMode
|
|
18
|
+
# The interpreter line, e.g. "#!/usr/bin/env python3". None in jazzoo mode.
|
|
19
|
+
shebang: T.Optional[str] = None
|
|
20
|
+
lines: T.List[BlockLine] = []
|
|
21
|
+
filename: T.Optional[str] = None
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .actions.run import run
|
|
2
|
+
from .actions.run_docker import run_docker
|
|
3
|
+
from .errors import (
|
|
4
|
+
CommandAbortedError,
|
|
5
|
+
CommandError,
|
|
6
|
+
CommandFailedError,
|
|
7
|
+
CommandNotFoundError,
|
|
8
|
+
)
|
|
9
|
+
from .models.command_result import CommandResult
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CommandAbortedError",
|
|
13
|
+
"CommandError",
|
|
14
|
+
"CommandFailedError",
|
|
15
|
+
"CommandNotFoundError",
|
|
16
|
+
"CommandResult",
|
|
17
|
+
"run",
|
|
18
|
+
"run_docker",
|
|
19
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typing as T
|
|
3
|
+
|
|
4
|
+
from ..errors import CommandAbortedError, CommandError
|
|
5
|
+
from ..settings import CONFIRM_PROMPT, TERMINAL
|
|
6
|
+
from ..utils.format_command import format_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def confirm_command(
|
|
10
|
+
command: T.Union[str, T.List[str]], cwd: T.Optional[str] = None
|
|
11
|
+
) -> bool:
|
|
12
|
+
try:
|
|
13
|
+
reader = open(TERMINAL, "r", encoding="utf-8")
|
|
14
|
+
writer = open(TERMINAL, "w", encoding="utf-8")
|
|
15
|
+
except OSError:
|
|
16
|
+
raise CommandError("--confirm needs a terminal")
|
|
17
|
+
|
|
18
|
+
with reader, writer:
|
|
19
|
+
# Asks again until the answer is y, n, q or Enter
|
|
20
|
+
while True:
|
|
21
|
+
writer.write(format_command(command, cwd or os.getcwd()) + "\n")
|
|
22
|
+
writer.write(CONFIRM_PROMPT)
|
|
23
|
+
writer.flush()
|
|
24
|
+
|
|
25
|
+
line = reader.readline()
|
|
26
|
+
# An empty line has a newline; no newline means the end of input
|
|
27
|
+
if not line:
|
|
28
|
+
raise CommandAbortedError("aborted by the user")
|
|
29
|
+
|
|
30
|
+
answer = line.strip().lower()
|
|
31
|
+
if answer in ("", "y"):
|
|
32
|
+
return True
|
|
33
|
+
if answer == "n":
|
|
34
|
+
return False
|
|
35
|
+
if answer == "q":
|
|
36
|
+
raise CommandAbortedError("aborted by the user")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import signal
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import typing as T
|
|
7
|
+
|
|
8
|
+
from ..errors import CommandFailedError
|
|
9
|
+
from ..models.command_result import CommandResult
|
|
10
|
+
from ..rules.find_executable import find_executable
|
|
11
|
+
from ..rules.is_confirm_on import is_confirm_on
|
|
12
|
+
from ..utils.format_command import format_command
|
|
13
|
+
from ..utils.ignoring_sigint import ignoring_sigint
|
|
14
|
+
from .confirm_command import confirm_command
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(
|
|
18
|
+
command: T.Union[str, T.List[str]],
|
|
19
|
+
cwd: T.Optional[str] = None,
|
|
20
|
+
env: T.Optional[T.Dict[str, str]] = None,
|
|
21
|
+
capture: bool = False,
|
|
22
|
+
check: bool = True,
|
|
23
|
+
echo: bool = False,
|
|
24
|
+
) -> CommandResult:
|
|
25
|
+
# Check the arguments before anything runs
|
|
26
|
+
if isinstance(command, str):
|
|
27
|
+
if not command.strip():
|
|
28
|
+
raise ValueError("the command is empty")
|
|
29
|
+
elif not command:
|
|
30
|
+
raise ValueError("the command is empty")
|
|
31
|
+
if cwd is not None and not os.path.isdir(cwd):
|
|
32
|
+
raise ValueError(f"directory not found: {cwd}")
|
|
33
|
+
|
|
34
|
+
environ = {**os.environ, **(env or {})}
|
|
35
|
+
|
|
36
|
+
# A string runs in bash; a list runs directly
|
|
37
|
+
if isinstance(command, str):
|
|
38
|
+
args = ["bash", "-c", command]
|
|
39
|
+
executable = shutil.which("bash") or "/bin/bash"
|
|
40
|
+
else:
|
|
41
|
+
args = list(command)
|
|
42
|
+
executable = find_executable(args[0], environ)
|
|
43
|
+
|
|
44
|
+
# The prompt shows the command, so it replaces the echo
|
|
45
|
+
if is_confirm_on():
|
|
46
|
+
if not confirm_command(command, cwd):
|
|
47
|
+
skipped_output = "" if capture else None
|
|
48
|
+
return CommandResult(
|
|
49
|
+
command=command,
|
|
50
|
+
returncode=0,
|
|
51
|
+
stdout=skipped_output,
|
|
52
|
+
stderr=skipped_output,
|
|
53
|
+
skipped=True,
|
|
54
|
+
)
|
|
55
|
+
elif echo:
|
|
56
|
+
print(format_command(command, cwd))
|
|
57
|
+
|
|
58
|
+
# The command writes to the same file descriptors as the script, so what the
|
|
59
|
+
# script printed before must come first
|
|
60
|
+
sys.stdout.flush()
|
|
61
|
+
sys.stderr.flush()
|
|
62
|
+
|
|
63
|
+
output = subprocess.PIPE if capture else None
|
|
64
|
+
process = subprocess.Popen(
|
|
65
|
+
args,
|
|
66
|
+
executable=executable,
|
|
67
|
+
cwd=cwd,
|
|
68
|
+
env=environ,
|
|
69
|
+
stdout=output,
|
|
70
|
+
stderr=output,
|
|
71
|
+
text=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# SIGINT is ignored only after the command has started, because an ignored
|
|
75
|
+
# signal would be inherited by the command. A ctrl-c then reaches the command
|
|
76
|
+
# only.
|
|
77
|
+
with ignoring_sigint():
|
|
78
|
+
stdout, stderr = process.communicate()
|
|
79
|
+
|
|
80
|
+
if process.returncode == -signal.SIGINT:
|
|
81
|
+
raise KeyboardInterrupt
|
|
82
|
+
|
|
83
|
+
result = CommandResult(
|
|
84
|
+
command=command,
|
|
85
|
+
returncode=process.returncode,
|
|
86
|
+
stdout=stdout,
|
|
87
|
+
stderr=stderr,
|
|
88
|
+
)
|
|
89
|
+
if check and result.returncode != 0:
|
|
90
|
+
raise CommandFailedError(_failure_message(result, cwd), result)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _failure_message(result: CommandResult, cwd: T.Optional[str]) -> str:
|
|
95
|
+
message = f"command failed with exit code {result.returncode}: " + format_command(
|
|
96
|
+
result.command, cwd
|
|
97
|
+
)
|
|
98
|
+
if result.stderr and result.stderr.strip():
|
|
99
|
+
message += "\n" + result.stderr.strip()
|
|
100
|
+
return message
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import typing as T
|
|
2
|
+
|
|
3
|
+
from ..models.command_result import CommandResult
|
|
4
|
+
from ..rules.needs_tty import needs_tty
|
|
5
|
+
|
|
6
|
+
Command = T.Union[str, T.List[str]]
|
|
7
|
+
RunFunction = T.Callable[..., CommandResult]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_docker(
|
|
11
|
+
container: str,
|
|
12
|
+
user: T.Optional[str] = None,
|
|
13
|
+
workdir: T.Optional[str] = None,
|
|
14
|
+
env: T.Optional[T.Dict[str, str]] = None,
|
|
15
|
+
shell: str = "bash",
|
|
16
|
+
) -> T.Callable[[RunFunction], RunFunction]:
|
|
17
|
+
if not container or not container.strip():
|
|
18
|
+
raise ValueError("the container is empty")
|
|
19
|
+
|
|
20
|
+
container_env = dict(env or {})
|
|
21
|
+
|
|
22
|
+
def decorator(run: RunFunction) -> RunFunction:
|
|
23
|
+
def run_in_container(
|
|
24
|
+
command: Command,
|
|
25
|
+
cwd: T.Optional[str] = None,
|
|
26
|
+
env: T.Optional[T.Dict[str, str]] = None,
|
|
27
|
+
capture: bool = False,
|
|
28
|
+
check: bool = True,
|
|
29
|
+
echo: bool = False,
|
|
30
|
+
) -> CommandResult:
|
|
31
|
+
# Check the command before the docker command hides it
|
|
32
|
+
if isinstance(command, str):
|
|
33
|
+
if not command.strip():
|
|
34
|
+
raise ValueError("the command is empty")
|
|
35
|
+
command_args = [shell, "-c", command]
|
|
36
|
+
elif not command:
|
|
37
|
+
raise ValueError("the command is empty")
|
|
38
|
+
else:
|
|
39
|
+
command_args = list(command)
|
|
40
|
+
|
|
41
|
+
# The options come in a fixed order
|
|
42
|
+
docker_command = ["docker", "exec", "--interactive"]
|
|
43
|
+
if needs_tty(capture):
|
|
44
|
+
docker_command.append("--tty")
|
|
45
|
+
if user:
|
|
46
|
+
docker_command += ["--user", user]
|
|
47
|
+
# A cwd only replaces the workdir for this call
|
|
48
|
+
call_workdir = cwd or workdir
|
|
49
|
+
if call_workdir:
|
|
50
|
+
docker_command += ["--workdir", call_workdir]
|
|
51
|
+
for name, value in {**container_env, **(env or {})}.items():
|
|
52
|
+
docker_command += ["--env", f"{name}={value}"]
|
|
53
|
+
docker_command += [container, *command_args]
|
|
54
|
+
|
|
55
|
+
# The cwd and env belong to the container, not to the docker client
|
|
56
|
+
return run(
|
|
57
|
+
docker_command,
|
|
58
|
+
cwd=None,
|
|
59
|
+
env=None,
|
|
60
|
+
capture=capture,
|
|
61
|
+
check=check,
|
|
62
|
+
echo=echo,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return run_in_container
|
|
66
|
+
|
|
67
|
+
return decorator
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import typing as T
|
|
4
|
+
|
|
5
|
+
from ..actions.run import run as run_command
|
|
6
|
+
from ..errors import (
|
|
7
|
+
CommandAbortedError,
|
|
8
|
+
CommandError,
|
|
9
|
+
CommandNotFoundError,
|
|
10
|
+
UsageError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
USAGE = (
|
|
14
|
+
"usage: jaz-run [--echo] [--] <executable> [<argument> ...]\n"
|
|
15
|
+
' jaz-run [--echo] -c "<command>"'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _ArgumentParser(argparse.ArgumentParser):
|
|
20
|
+
def error(self, message: str):
|
|
21
|
+
raise UsageError(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_args(argv: T.Optional[T.List[str]] = None):
|
|
25
|
+
parser = _ArgumentParser(prog="jaz-run", add_help=False)
|
|
26
|
+
parser.add_argument("--echo", dest="echo", action="store_true")
|
|
27
|
+
parser.add_argument("-c", dest="string_command", default=None)
|
|
28
|
+
parser.add_argument("--help", "-h", dest="help", action="store_true")
|
|
29
|
+
# The first argument that is not an option of jaz-run starts the command
|
|
30
|
+
parser.add_argument("command", nargs=argparse.REMAINDER)
|
|
31
|
+
return parser.parse_args(argv)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_command(args) -> T.Union[str, T.List[str]]:
|
|
35
|
+
command = list(args.command)
|
|
36
|
+
if command[:1] == ["--"]:
|
|
37
|
+
command = command[1:]
|
|
38
|
+
|
|
39
|
+
if args.string_command is not None:
|
|
40
|
+
if command:
|
|
41
|
+
raise UsageError("-c takes a single command")
|
|
42
|
+
return args.string_command
|
|
43
|
+
|
|
44
|
+
if not command:
|
|
45
|
+
raise UsageError("no command given")
|
|
46
|
+
return command
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run(argv: T.Optional[T.List[str]] = None) -> int:
|
|
50
|
+
args = _parse_args(argv)
|
|
51
|
+
if args.help:
|
|
52
|
+
print(USAGE)
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
result = run_command(_get_command(args), check=False, echo=args.echo)
|
|
56
|
+
return result.returncode
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: T.Optional[T.List[str]] = None) -> int:
|
|
60
|
+
try:
|
|
61
|
+
return run(argv)
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
return 130
|
|
64
|
+
except CommandAbortedError as error:
|
|
65
|
+
print(f"jaz-run: {error}", file=sys.stderr)
|
|
66
|
+
return 130
|
|
67
|
+
except CommandNotFoundError as error:
|
|
68
|
+
print(f"jaz-run: {error}", file=sys.stderr)
|
|
69
|
+
return 127
|
|
70
|
+
except (CommandError, ValueError) as error:
|
|
71
|
+
print(f"jaz-run: {error}", file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
sys.exit(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import typing as T
|
|
2
|
+
|
|
3
|
+
if T.TYPE_CHECKING:
|
|
4
|
+
from .models.command_result import CommandResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommandError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandNotFoundError(CommandError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandAbortedError(CommandError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UsageError(CommandError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommandFailedError(CommandError):
|
|
24
|
+
def __init__(self, message: str, result: "CommandResult"):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.result = result
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import typing as T
|
|
2
|
+
|
|
3
|
+
from ..utils.dataclass import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CommandResult:
|
|
8
|
+
# The command, as it was given to run
|
|
9
|
+
command: T.Union[str, T.List[str]]
|
|
10
|
+
returncode: int
|
|
11
|
+
# None unless the output was captured
|
|
12
|
+
stdout: T.Optional[str] = None
|
|
13
|
+
stderr: T.Optional[str] = None
|
|
14
|
+
# True when the user skipped the command at the confirmation prompt
|
|
15
|
+
skipped: bool = False
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import typing as T
|
|
4
|
+
|
|
5
|
+
from ..errors import CommandNotFoundError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_executable(name: str, environ: T.Dict[str, str]) -> str:
|
|
9
|
+
# A name with a slash is a path, as in a shell
|
|
10
|
+
if "/" in name:
|
|
11
|
+
if os.path.isfile(name) and os.access(name, os.X_OK):
|
|
12
|
+
return name
|
|
13
|
+
raise CommandNotFoundError(f"command not found: {name}")
|
|
14
|
+
|
|
15
|
+
path = shutil.which(name, path=environ.get("PATH", os.defpath))
|
|
16
|
+
if path is None:
|
|
17
|
+
raise CommandNotFoundError(f"command not found: {name}")
|
|
18
|
+
return path
|