zendt 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.
- zendt/__init__.py +3 -0
- zendt/__main__.py +4 -0
- zendt/cli.py +167 -0
- zendt/config/__init__.py +0 -0
- zendt/config/create.py +166 -0
- zendt/config/edit.py +252 -0
- zendt/config/helper.py +26 -0
- zendt/engine/__init__.py +0 -0
- zendt/engine/evaluate.py +306 -0
- zendt/engine/helpers.py +25 -0
- zendt/engine/input.py +102 -0
- zendt/engine/load.py +60 -0
- zendt/engine/output.py +34 -0
- zendt/errors.py +38 -0
- zendt/main.py +76 -0
- zendt/models.py +131 -0
- zendt/rules/__init__.py +0 -0
- zendt/rules/catalog.py +227 -0
- zendt/rules/check_jdm.py +16 -0
- zendt/rules/configs.py +335 -0
- zendt/rules/sources.py +173 -0
- zendt/utils/__init__.py +0 -0
- zendt/utils/env.py +18 -0
- zendt/utils/paths.py +30 -0
- zendt-0.1.0.dist-info/METADATA +200 -0
- zendt-0.1.0.dist-info/RECORD +29 -0
- zendt-0.1.0.dist-info/WHEEL +4 -0
- zendt-0.1.0.dist-info/entry_points.txt +3 -0
- zendt-0.1.0.dist-info/licenses/LICENSE +21 -0
zendt/__init__.py
ADDED
zendt/__main__.py
ADDED
zendt/cli.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from zendt.config.create import create_config
|
|
8
|
+
from zendt.config.edit import edit_config
|
|
9
|
+
from zendt.engine.output import write_output
|
|
10
|
+
from zendt.errors import ZendtError
|
|
11
|
+
from zendt.main import apply, flow, rules_list
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name='zendt',
|
|
15
|
+
help='Apply JDM (GoRules) rules to data from the terminal.',
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
SourceOption = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
'--source',
|
|
22
|
+
'-s',
|
|
23
|
+
help='GitHub URL, local path, or id of a configured source.',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
FindOption = typer.Option(
|
|
27
|
+
None,
|
|
28
|
+
'--find',
|
|
29
|
+
'-f',
|
|
30
|
+
help='Search for rules whose name contains the specified word.',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
VersionOption = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
'--version',
|
|
36
|
+
'-v',
|
|
37
|
+
help='Rule version to apply for git repositories.',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
AuthOption = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
'--authentication',
|
|
43
|
+
'-a',
|
|
44
|
+
help=(
|
|
45
|
+
'Name of the environment variable that holds the token '
|
|
46
|
+
'(private repositories).'
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
InputOption = typer.Option(
|
|
51
|
+
None,
|
|
52
|
+
'--input',
|
|
53
|
+
'-i',
|
|
54
|
+
help='Input file to process.',
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
OutputOption = typer.Option(
|
|
58
|
+
None,
|
|
59
|
+
'--output',
|
|
60
|
+
'-o',
|
|
61
|
+
help='Output file to save.',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
PrintHeadOption = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
'--printhead',
|
|
67
|
+
'-ph',
|
|
68
|
+
help='Print a preview of the result on screen.',
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
CreateConfigOption = typer.Option(
|
|
72
|
+
False,
|
|
73
|
+
'--create',
|
|
74
|
+
'-c',
|
|
75
|
+
help='Create a zendt.json configuration file.',
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
EditConfigOption = typer.Option(
|
|
79
|
+
False,
|
|
80
|
+
'--edit',
|
|
81
|
+
'-e',
|
|
82
|
+
help='Edit the zendt.json configuration file.',
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def echo_head(frame: pd.DataFrame) -> None:
|
|
87
|
+
"""Preview on the terminal.
|
|
88
|
+
|
|
89
|
+
Keep it off stdout when that stream carries the csv.
|
|
90
|
+
"""
|
|
91
|
+
typer.echo(frame.head(), err=not sys.stdout.isatty())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.command('config')
|
|
95
|
+
def config(
|
|
96
|
+
create: bool = CreateConfigOption,
|
|
97
|
+
edit: bool = EditConfigOption,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Manage the zendt.json configuration file."""
|
|
100
|
+
if create:
|
|
101
|
+
create_config()
|
|
102
|
+
elif edit:
|
|
103
|
+
edit_config()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command('list')
|
|
107
|
+
def list_rules_command(
|
|
108
|
+
source: str = SourceOption,
|
|
109
|
+
find: str = FindOption,
|
|
110
|
+
version: str = VersionOption,
|
|
111
|
+
authentication: str = AuthOption,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""List the JDM rules found in SOURCE."""
|
|
114
|
+
try:
|
|
115
|
+
names = rules_list(source, find, version, authentication)
|
|
116
|
+
except ZendtError as exc:
|
|
117
|
+
typer.echo(str(exc), err=True)
|
|
118
|
+
raise typer.Exit(code=exc.exit_code) from exc
|
|
119
|
+
|
|
120
|
+
typer.echo('Rules found in provided sources:')
|
|
121
|
+
for name in names:
|
|
122
|
+
typer.echo(name)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command('apply')
|
|
126
|
+
def apply_command(
|
|
127
|
+
rules: Annotated[
|
|
128
|
+
list[str], typer.Argument(help='Names of the rules to apply.')
|
|
129
|
+
],
|
|
130
|
+
source: str = SourceOption,
|
|
131
|
+
authentication: str = AuthOption,
|
|
132
|
+
version: str = VersionOption,
|
|
133
|
+
input: str = InputOption,
|
|
134
|
+
output: str = OutputOption,
|
|
135
|
+
printhead: bool = PrintHeadOption,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Apply the JDM rules passed as arguments."""
|
|
138
|
+
try:
|
|
139
|
+
result = apply(rules, source, authentication, version, input)
|
|
140
|
+
write_output(result, output)
|
|
141
|
+
if printhead:
|
|
142
|
+
echo_head(result)
|
|
143
|
+
except ZendtError as exc:
|
|
144
|
+
typer.echo(str(exc), err=True)
|
|
145
|
+
raise typer.Exit(code=exc.exit_code) from exc
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command('flow')
|
|
149
|
+
def flow_command(
|
|
150
|
+
flow_id: Annotated[str, typer.Argument(help='Configured flow ID.')],
|
|
151
|
+
input: str = InputOption,
|
|
152
|
+
output: str = OutputOption,
|
|
153
|
+
printhead: bool = PrintHeadOption,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Apply a pre-configured flow."""
|
|
156
|
+
try:
|
|
157
|
+
result = flow(flow_id, input)
|
|
158
|
+
except ZendtError as exc:
|
|
159
|
+
typer.echo(str(exc), err=True)
|
|
160
|
+
raise typer.Exit(code=exc.exit_code) from exc
|
|
161
|
+
|
|
162
|
+
destination = output
|
|
163
|
+
if destination is None:
|
|
164
|
+
destination = result.attrs.get('output')
|
|
165
|
+
write_output(result, destination)
|
|
166
|
+
if printhead:
|
|
167
|
+
echo_head(result)
|
zendt/config/__init__.py
ADDED
|
File without changes
|
zendt/config/create.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from zendt.config.helper import save_config
|
|
6
|
+
from zendt.utils.paths import is_remote
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_source() -> dict:
|
|
10
|
+
"""Create a new source."""
|
|
11
|
+
typer.echo('Enter a unique name for your source (ID):')
|
|
12
|
+
source_id = typer.prompt('Source ID')
|
|
13
|
+
|
|
14
|
+
typer.echo('Enter the source location (local path or git URL):')
|
|
15
|
+
source_location = typer.prompt('Source location')
|
|
16
|
+
|
|
17
|
+
if is_remote(source_location):
|
|
18
|
+
is_private = typer.confirm('Is this a private repository?')
|
|
19
|
+
if is_private:
|
|
20
|
+
typer.echo(
|
|
21
|
+
'Enter the name of the environment variable for the '
|
|
22
|
+
'authentication token [do not enter the token]:'
|
|
23
|
+
)
|
|
24
|
+
source_authentication = typer.prompt('Source Authentication')
|
|
25
|
+
else:
|
|
26
|
+
source_authentication = None
|
|
27
|
+
|
|
28
|
+
version = typer.confirm(
|
|
29
|
+
'Would you like to specify a version for this source? '
|
|
30
|
+
'If no, the ref in the URL is used, or HEAD when the URL has none.'
|
|
31
|
+
)
|
|
32
|
+
if version:
|
|
33
|
+
typer.echo('Enter the source version:')
|
|
34
|
+
source_version = typer.prompt('Source Version')
|
|
35
|
+
else:
|
|
36
|
+
source_version = None
|
|
37
|
+
else:
|
|
38
|
+
source_authentication = None
|
|
39
|
+
source_version = None
|
|
40
|
+
|
|
41
|
+
source = {}
|
|
42
|
+
source['id'] = source_id
|
|
43
|
+
source['source'] = source_location
|
|
44
|
+
if source_authentication:
|
|
45
|
+
source['authentication'] = source_authentication
|
|
46
|
+
if source_version:
|
|
47
|
+
source['version'] = source_version
|
|
48
|
+
|
|
49
|
+
return source
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def take_source(sources: list[dict], source: dict) -> bool:
|
|
53
|
+
"""Append a source when its id is usable.
|
|
54
|
+
|
|
55
|
+
False leaves the list unchanged.
|
|
56
|
+
"""
|
|
57
|
+
source_id = source.get('id', '')
|
|
58
|
+
if not source_id or '/' in source_id:
|
|
59
|
+
typer.echo(
|
|
60
|
+
"Source id is required and cannot contain '/'. "
|
|
61
|
+
'Source was not added.'
|
|
62
|
+
)
|
|
63
|
+
return False
|
|
64
|
+
if any(item.get('id') == source_id for item in sources):
|
|
65
|
+
typer.echo(
|
|
66
|
+
f"Source '{source_id}' already exists. Source was not added."
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
69
|
+
sources.append(source)
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_flow() -> dict:
|
|
74
|
+
"""Create a new flow."""
|
|
75
|
+
flow_id = typer.prompt('Enter a unique name for your flow (ID):')
|
|
76
|
+
flow_description = typer.prompt('Describe your flow in a few words:')
|
|
77
|
+
|
|
78
|
+
flow_rules = [
|
|
79
|
+
rule.strip()
|
|
80
|
+
for rule in typer.prompt(
|
|
81
|
+
'Enter the rule(s) for your flow, comma separated:'
|
|
82
|
+
).split(',')
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
input_file = typer.confirm(
|
|
86
|
+
'Would you like to specify a input file for your flow? '
|
|
87
|
+
'Accepted formats: csv, csv.gz, xlsx, json'
|
|
88
|
+
)
|
|
89
|
+
if input_file:
|
|
90
|
+
flow_input = typer.prompt('Flow Input Path [example: data/input.csv]:')
|
|
91
|
+
else:
|
|
92
|
+
flow_input = None
|
|
93
|
+
|
|
94
|
+
output_file = typer.confirm(
|
|
95
|
+
'Would you like to specify a output file for your flow? '
|
|
96
|
+
'Accepted formats: csv, csv.gz, xlsx, json'
|
|
97
|
+
)
|
|
98
|
+
if output_file:
|
|
99
|
+
flow_output = typer.prompt(
|
|
100
|
+
'Flow Output Path [example: data/output.csv]:'
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
flow_output = None
|
|
104
|
+
|
|
105
|
+
flow = {}
|
|
106
|
+
flow['id'] = flow_id
|
|
107
|
+
flow['description'] = flow_description
|
|
108
|
+
flow['rules'] = flow_rules
|
|
109
|
+
if flow_input:
|
|
110
|
+
flow['input'] = flow_input
|
|
111
|
+
if flow_output:
|
|
112
|
+
flow['output'] = flow_output
|
|
113
|
+
|
|
114
|
+
return flow
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def take_flow(flows: list[dict], flow: dict) -> bool:
|
|
118
|
+
"""Append a flow when its id and rules are usable.
|
|
119
|
+
|
|
120
|
+
False leaves the list unchanged.
|
|
121
|
+
"""
|
|
122
|
+
flow_id = flow.get('id', '')
|
|
123
|
+
if not flow_id:
|
|
124
|
+
typer.echo('Flow id is required. Flow was not added.')
|
|
125
|
+
return False
|
|
126
|
+
if any(item.get('id') == flow_id for item in flows):
|
|
127
|
+
typer.echo(f"Flow '{flow_id}' already exists. Flow was not added.")
|
|
128
|
+
return False
|
|
129
|
+
flow['rules'] = [rule for rule in flow.get('rules') or [] if rule]
|
|
130
|
+
if not flow['rules']:
|
|
131
|
+
typer.echo('A flow needs at least one rule. Flow was not added.')
|
|
132
|
+
return False
|
|
133
|
+
flows.append(flow)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def create_config() -> None:
|
|
138
|
+
"""Create a new config file."""
|
|
139
|
+
|
|
140
|
+
path = Path.cwd() / 'zendt.json'
|
|
141
|
+
if path.exists():
|
|
142
|
+
if not typer.confirm(
|
|
143
|
+
f'Config file already exists at {path}. '
|
|
144
|
+
'Would you like to overwrite it?'
|
|
145
|
+
):
|
|
146
|
+
typer.echo('Config file not overwritten.')
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
config = {
|
|
150
|
+
'sources': [],
|
|
151
|
+
'flows': [],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
typer.echo("Let's create the first source...")
|
|
155
|
+
while True:
|
|
156
|
+
if take_source(config['sources'], create_source()):
|
|
157
|
+
if not typer.confirm('Do you want to add another source?'):
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
if typer.confirm('Would you like to create a flow?'):
|
|
161
|
+
while True:
|
|
162
|
+
if take_flow(config['flows'], create_flow()):
|
|
163
|
+
if not typer.confirm('Do you want to add another flow?'):
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
save_config(config)
|
zendt/config/edit.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from zendt.config.create import (
|
|
7
|
+
create_flow,
|
|
8
|
+
create_source,
|
|
9
|
+
take_flow,
|
|
10
|
+
take_source,
|
|
11
|
+
)
|
|
12
|
+
from zendt.config.helper import save_config
|
|
13
|
+
from zendt.utils.paths import is_remote
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def open_config() -> dict | None:
|
|
17
|
+
"""Load zendt.json from the current directory."""
|
|
18
|
+
path = Path.cwd() / 'zendt.json'
|
|
19
|
+
if not path.exists():
|
|
20
|
+
typer.echo(
|
|
21
|
+
'Config file not found. Please create it first using '
|
|
22
|
+
"'zendt config --create'."
|
|
23
|
+
)
|
|
24
|
+
return None
|
|
25
|
+
try:
|
|
26
|
+
config = json.loads(path.read_text(encoding='utf-8'))
|
|
27
|
+
except Exception as exc:
|
|
28
|
+
typer.echo(f'Error loading config file: {exc}')
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
if not isinstance(config, dict) or not isinstance(
|
|
32
|
+
config.get('sources'), list
|
|
33
|
+
):
|
|
34
|
+
typer.echo("Config file is missing a 'sources' array.")
|
|
35
|
+
return None
|
|
36
|
+
if 'flows' not in config:
|
|
37
|
+
config['flows'] = []
|
|
38
|
+
elif not isinstance(config['flows'], list):
|
|
39
|
+
typer.echo("Config file 'flows' must be an array.")
|
|
40
|
+
return None
|
|
41
|
+
return config
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def edit_config() -> None:
|
|
45
|
+
"""Edit sources and flows in zendt.json."""
|
|
46
|
+
config = open_config()
|
|
47
|
+
if config is None:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
typer.echo('What would you like to edit?')
|
|
52
|
+
typer.echo('1. Sources')
|
|
53
|
+
typer.echo('2. Flows')
|
|
54
|
+
typer.echo('3. Save and exit')
|
|
55
|
+
choice = typer.prompt('Enter your choice').strip()
|
|
56
|
+
if choice == '1':
|
|
57
|
+
edit_sources(config)
|
|
58
|
+
elif choice == '2':
|
|
59
|
+
edit_flows(config)
|
|
60
|
+
elif choice == '3':
|
|
61
|
+
if save_config(config):
|
|
62
|
+
return
|
|
63
|
+
else:
|
|
64
|
+
typer.echo('Invalid choice.')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def edit_sources(config: dict) -> None:
|
|
68
|
+
"""Create, delete, or edit a source."""
|
|
69
|
+
list_items(config['sources'], 'sources')
|
|
70
|
+
typer.echo('1. Create')
|
|
71
|
+
typer.echo('2. Delete')
|
|
72
|
+
typer.echo('3. Edit')
|
|
73
|
+
typer.echo('4. Back')
|
|
74
|
+
choice = typer.prompt('Enter your choice').strip()
|
|
75
|
+
if choice == '1':
|
|
76
|
+
add_source(config)
|
|
77
|
+
elif choice == '2':
|
|
78
|
+
delete_by_id(config['sources'], 'source', keep_one=True)
|
|
79
|
+
elif choice == '3':
|
|
80
|
+
edit_source(config)
|
|
81
|
+
elif choice == '4':
|
|
82
|
+
return
|
|
83
|
+
else:
|
|
84
|
+
typer.echo('Invalid choice.')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def edit_flows(config: dict) -> None:
|
|
88
|
+
"""Create, delete, or edit a flow."""
|
|
89
|
+
list_items(config['flows'], 'flows')
|
|
90
|
+
typer.echo('1. Create')
|
|
91
|
+
typer.echo('2. Delete')
|
|
92
|
+
typer.echo('3. Edit')
|
|
93
|
+
typer.echo('4. Back')
|
|
94
|
+
choice = typer.prompt('Enter your choice').strip()
|
|
95
|
+
if choice == '1':
|
|
96
|
+
add_flow(config)
|
|
97
|
+
elif choice == '2':
|
|
98
|
+
delete_by_id(config['flows'], 'flow')
|
|
99
|
+
elif choice == '3':
|
|
100
|
+
edit_flow(config)
|
|
101
|
+
elif choice == '4':
|
|
102
|
+
return
|
|
103
|
+
else:
|
|
104
|
+
typer.echo('Invalid choice.')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def list_items(items: list[dict], kind: str) -> None:
|
|
108
|
+
if not items:
|
|
109
|
+
typer.echo(f'No {kind} yet.')
|
|
110
|
+
return
|
|
111
|
+
typer.echo(f'Current {kind}:')
|
|
112
|
+
for item in items:
|
|
113
|
+
item_id = item.get('id', '?')
|
|
114
|
+
if kind == 'sources':
|
|
115
|
+
typer.echo(f' {item_id}: {item.get("source", "")}')
|
|
116
|
+
else:
|
|
117
|
+
rules = ', '.join(item.get('rules') or [])
|
|
118
|
+
typer.echo(f' {item_id}: {rules}')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def find_by_id(items: list[dict], item_id: str) -> dict | None:
|
|
122
|
+
for item in items:
|
|
123
|
+
if item.get('id') == item_id:
|
|
124
|
+
return item
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def add_source(config: dict) -> None:
|
|
129
|
+
take_source(config['sources'], create_source())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def add_flow(config: dict) -> None:
|
|
133
|
+
take_flow(config['flows'], create_flow())
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def delete_by_id(
|
|
137
|
+
items: list[dict], kind: str, *, keep_one: bool = False
|
|
138
|
+
) -> None:
|
|
139
|
+
item_id = typer.prompt(f'Enter the {kind} ID').strip()
|
|
140
|
+
item = find_by_id(items, item_id)
|
|
141
|
+
if item is None:
|
|
142
|
+
typer.echo(f'{kind.capitalize()} not found.')
|
|
143
|
+
return
|
|
144
|
+
if keep_one and len(items) <= 1:
|
|
145
|
+
typer.echo(
|
|
146
|
+
'At least one source is required. Add another source '
|
|
147
|
+
'before deleting this one.'
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
if not typer.confirm(f"Delete {kind} '{item_id}'?"):
|
|
151
|
+
typer.echo(f'{kind.capitalize()} was not deleted.')
|
|
152
|
+
return
|
|
153
|
+
items.remove(item)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def prompt_keep(
|
|
157
|
+
label: str, current: str | None, *, allow_clear: bool = False
|
|
158
|
+
) -> str | None:
|
|
159
|
+
"""Ask for a new value. Enter keeps the current one.
|
|
160
|
+
|
|
161
|
+
'-' clears optional fields.
|
|
162
|
+
"""
|
|
163
|
+
shown = current if current else '(none)'
|
|
164
|
+
hint = "enter to keep, '-' to clear" if allow_clear else 'enter to keep'
|
|
165
|
+
value = typer.prompt(
|
|
166
|
+
f'{label} [{hint}: {shown}]',
|
|
167
|
+
default=current or '',
|
|
168
|
+
show_default=False,
|
|
169
|
+
).strip()
|
|
170
|
+
if allow_clear and value == '-':
|
|
171
|
+
return None
|
|
172
|
+
if not value:
|
|
173
|
+
return current
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def set_optional(item: dict, key: str, value: str | None) -> None:
|
|
178
|
+
if value:
|
|
179
|
+
item[key] = value
|
|
180
|
+
else:
|
|
181
|
+
item.pop(key, None)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def edit_source(config: dict) -> None:
|
|
185
|
+
source_id = typer.prompt('Enter the source ID').strip()
|
|
186
|
+
source = find_by_id(config['sources'], source_id)
|
|
187
|
+
if source is None:
|
|
188
|
+
typer.echo('Source not found.')
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
typer.echo(f'Editing source {source_id}...')
|
|
192
|
+
location = prompt_keep('Source location', source.get('source'))
|
|
193
|
+
if not location:
|
|
194
|
+
typer.echo('Source location is required. This source was not changed.')
|
|
195
|
+
return
|
|
196
|
+
source['source'] = location
|
|
197
|
+
|
|
198
|
+
if not is_remote(location):
|
|
199
|
+
had_remote_fields = 'authentication' in source or 'version' in source
|
|
200
|
+
source.pop('authentication', None)
|
|
201
|
+
source.pop('version', None)
|
|
202
|
+
if had_remote_fields:
|
|
203
|
+
typer.echo(
|
|
204
|
+
'Authentication and version were removed because '
|
|
205
|
+
'this source is local.'
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
authentication = prompt_keep(
|
|
210
|
+
'Source authentication (env var name, not the token)',
|
|
211
|
+
source.get('authentication'),
|
|
212
|
+
allow_clear=True,
|
|
213
|
+
)
|
|
214
|
+
version = prompt_keep(
|
|
215
|
+
'Source version', source.get('version'), allow_clear=True
|
|
216
|
+
)
|
|
217
|
+
set_optional(source, 'authentication', authentication)
|
|
218
|
+
set_optional(source, 'version', version)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def edit_flow(config: dict) -> None:
|
|
222
|
+
flow_id = typer.prompt('Enter the flow ID').strip()
|
|
223
|
+
flow = find_by_id(config['flows'], flow_id)
|
|
224
|
+
if flow is None:
|
|
225
|
+
typer.echo('Flow not found.')
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
typer.echo(f'Editing flow {flow_id}...')
|
|
229
|
+
description = prompt_keep(
|
|
230
|
+
'Description', flow.get('description'), allow_clear=True
|
|
231
|
+
)
|
|
232
|
+
rules_value = prompt_keep(
|
|
233
|
+
'Rules (comma separated)', ', '.join(flow.get('rules') or [])
|
|
234
|
+
)
|
|
235
|
+
rules = [
|
|
236
|
+
part.strip() for part in (rules_value or '').split(',') if part.strip()
|
|
237
|
+
]
|
|
238
|
+
if not rules:
|
|
239
|
+
typer.echo(
|
|
240
|
+
'A flow needs at least one rule. This flow was not changed.'
|
|
241
|
+
)
|
|
242
|
+
return
|
|
243
|
+
set_optional(flow, 'description', description)
|
|
244
|
+
flow['rules'] = rules
|
|
245
|
+
flow_input = prompt_keep(
|
|
246
|
+
'Input path', flow.get('input'), allow_clear=True
|
|
247
|
+
)
|
|
248
|
+
flow_output = prompt_keep(
|
|
249
|
+
'Output path', flow.get('output'), allow_clear=True
|
|
250
|
+
)
|
|
251
|
+
set_optional(flow, 'input', flow_input)
|
|
252
|
+
set_optional(flow, 'output', flow_output)
|
zendt/config/helper.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from zendt.errors import ConfigError
|
|
7
|
+
from zendt.rules.configs import validate_parameters
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def save_config(config: dict) -> bool:
|
|
11
|
+
"""Write zendt.json. Returns False when the file was left unchanged."""
|
|
12
|
+
try:
|
|
13
|
+
validate_parameters(config)
|
|
14
|
+
except ConfigError as exc:
|
|
15
|
+
typer.echo(str(exc))
|
|
16
|
+
typer.echo('Config file was not saved.')
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
path = Path.cwd() / 'zendt.json'
|
|
20
|
+
try:
|
|
21
|
+
path.write_text(json.dumps(config, indent=4), encoding='utf-8')
|
|
22
|
+
except Exception as exc:
|
|
23
|
+
typer.echo(f'Error writing config file: {exc}')
|
|
24
|
+
return False
|
|
25
|
+
typer.echo('Saved zendt.json.')
|
|
26
|
+
return True
|
zendt/engine/__init__.py
ADDED
|
File without changes
|