gluekit 1.0.1.dev1__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.
- gluekit/__init__.py +7 -0
- gluekit/app.py +0 -0
- gluekit/cli.py +64 -0
- gluekit/commands/__init__.py +1 -0
- gluekit/commands/add.py +455 -0
- gluekit/commands/build.py +816 -0
- gluekit/commands/checkout.py +114 -0
- gluekit/commands/clone.py +516 -0
- gluekit/commands/config_commands.py +180 -0
- gluekit/commands/constants.py +47 -0
- gluekit/commands/convert.py +336 -0
- gluekit/commands/edit.py +1104 -0
- gluekit/commands/helpers.py +1068 -0
- gluekit/commands/init.py +798 -0
- gluekit/commands/list.py +16 -0
- gluekit/commands/local_commands.py +680 -0
- gluekit/commands/pull.py +374 -0
- gluekit/commands/push.py +251 -0
- gluekit/commands/remove.py +161 -0
- gluekit/commands/run.py +126 -0
- gluekit/commands/status.py +97 -0
- gluekit/commands/sync.py +97 -0
- gluekit/commands/update.py +104 -0
- gluekit/job_mgmt/__init__.py +0 -0
- gluekit/job_mgmt/glue_jobs.py +1323 -0
- gluekit/job_mgmt/magics.py +122 -0
- gluekit/job_mgmt/resources/__init__.py +0 -0
- gluekit/job_mgmt/resources/glue_job_schema.json +40341 -0
- gluekit/job_mgmt/resources/magic_map.json +83 -0
- gluekit/job_mgmt/schema.py +165 -0
- gluekit/local/__init__.py +6 -0
- gluekit/local/awsglue/__init__.py +1 -0
- gluekit/local/awsglue/context.py +30 -0
- gluekit/local/awsglue/job.py +9 -0
- gluekit/local/awsglue/utils.py +17 -0
- gluekit/local/local.py +434 -0
- gluekit/local/local_fixtures.py +337 -0
- gluekit/local/pyspark/__init__.py +7 -0
- gluekit/local/pyspark/context.py +31 -0
- gluekit/local/pyspark/sql/__init__.py +6 -0
- gluekit/local/pyspark/sql/session.py +29 -0
- gluekit-1.0.1.dev1.dist-info/METADATA +1176 -0
- gluekit-1.0.1.dev1.dist-info/RECORD +46 -0
- gluekit-1.0.1.dev1.dist-info/WHEEL +5 -0
- gluekit-1.0.1.dev1.dist-info/entry_points.txt +2 -0
- gluekit-1.0.1.dev1.dist-info/top_level.txt +1 -0
gluekit/__init__.py
ADDED
gluekit/app.py
ADDED
|
File without changes
|
gluekit/cli.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ruff: noqa: E402,F401,F403
|
|
2
|
+
"""Gluekit CLI entry point for local-first Glue job workflows.
|
|
3
|
+
|
|
4
|
+
The current command surface centers on a single active checkout context:
|
|
5
|
+
- `gluekit checkout <job-name>` selects the default local target.
|
|
6
|
+
- `gluekit edit` is the primary command for explicit local config mutations.
|
|
7
|
+
- `gluekit add` and `gluekit update` remain as compatibility shims.
|
|
8
|
+
- `gluekit run` executes Glue scripts locally with runtime emulators.
|
|
9
|
+
- `gluekit sync`, `gluekit convert`, and `gluekit remove` can also default to it.
|
|
10
|
+
- `gluekit pull` and `gluekit push` still support explicit job names and `*`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(no_args_is_help=True, rich_markup_mode="markdown")
|
|
18
|
+
glue_config_app = typer.Typer(
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
rich_markup_mode="markdown",
|
|
21
|
+
help="Show and set local reusable Glue config parameters.",
|
|
22
|
+
)
|
|
23
|
+
app.add_typer(glue_config_app, name="config")
|
|
24
|
+
glue_local_app = typer.Typer(
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
rich_markup_mode="markdown",
|
|
27
|
+
help="Manage local-only Glue development setups and mocked AWS fixtures.",
|
|
28
|
+
)
|
|
29
|
+
app.add_typer(glue_local_app, name="local")
|
|
30
|
+
|
|
31
|
+
# Import all commands to register them
|
|
32
|
+
from .commands import (
|
|
33
|
+
build,
|
|
34
|
+
checkout,
|
|
35
|
+
clone,
|
|
36
|
+
config_commands,
|
|
37
|
+
convert,
|
|
38
|
+
edit,
|
|
39
|
+
init,
|
|
40
|
+
list,
|
|
41
|
+
local_commands,
|
|
42
|
+
pull,
|
|
43
|
+
push,
|
|
44
|
+
run,
|
|
45
|
+
remove,
|
|
46
|
+
status,
|
|
47
|
+
sync,
|
|
48
|
+
add,
|
|
49
|
+
update,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = ["app", "glue_config_app", "glue_local_app"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
from .commands.constants import *
|
|
56
|
+
from .commands.helpers import *
|
|
57
|
+
from .commands.config_commands import (
|
|
58
|
+
_coerce_set_value,
|
|
59
|
+
_parse_set_args,
|
|
60
|
+
_to_csv_if_list,
|
|
61
|
+
_set_if_changed,
|
|
62
|
+
_write_config_changes,
|
|
63
|
+
)
|
|
64
|
+
from .commands.build import run_project_build, _build_zip_artifacts_from_sdists
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
gluekit/commands/add.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from slugify import slugify
|
|
8
|
+
|
|
9
|
+
from ..job_mgmt.glue_jobs import _resolve_notebook_path
|
|
10
|
+
from .helpers import (
|
|
11
|
+
_derive_s3_target,
|
|
12
|
+
_emit_compatibility_notice,
|
|
13
|
+
_examples_epilog,
|
|
14
|
+
_get_checked_out_jobs,
|
|
15
|
+
_load_config_index,
|
|
16
|
+
_looks_like_remote_module_spec,
|
|
17
|
+
_raise_missing_local_config,
|
|
18
|
+
_resolve_single_job_name,
|
|
19
|
+
_routes_to_additional_python_modules,
|
|
20
|
+
_write_config_changes,
|
|
21
|
+
)
|
|
22
|
+
from .edit import (
|
|
23
|
+
_append_csv_items,
|
|
24
|
+
_parse_csv_list,
|
|
25
|
+
_remove_csv_items,
|
|
26
|
+
)
|
|
27
|
+
from .convert import _update_notebook_config_cell
|
|
28
|
+
from ..cli import app
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_glue_add_target(
|
|
32
|
+
args: list[str],
|
|
33
|
+
*,
|
|
34
|
+
job_name: Optional[str],
|
|
35
|
+
) -> tuple[str, list[str]]:
|
|
36
|
+
# Keep `gluekit add` context-first: it uses the active checked-out job by
|
|
37
|
+
# default, and `--job-name` is a one-command override similar to `gh --repo`.
|
|
38
|
+
if not args:
|
|
39
|
+
raise typer.BadParameter("Provide one or more paths or PyPI modules.")
|
|
40
|
+
|
|
41
|
+
if job_name:
|
|
42
|
+
return job_name, args
|
|
43
|
+
|
|
44
|
+
resolved_job_name = _resolve_single_job_name(None, "glue add")
|
|
45
|
+
return resolved_job_name, args
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_package_wheel_path(dist_dir: Path = Path("dist")) -> Path:
|
|
49
|
+
if not dist_dir.exists():
|
|
50
|
+
raise typer.BadParameter(f"Package wheel directory not found: {dist_dir}")
|
|
51
|
+
if not dist_dir.is_dir():
|
|
52
|
+
raise typer.BadParameter(f"Package wheel path is not a directory: {dist_dir}")
|
|
53
|
+
|
|
54
|
+
wheel_paths = sorted(dist_dir.glob("*.whl"))
|
|
55
|
+
if not wheel_paths:
|
|
56
|
+
raise typer.BadParameter(f"No package wheel found matching {dist_dir}/*.whl")
|
|
57
|
+
if len(wheel_paths) > 1:
|
|
58
|
+
rendered = ", ".join(path.as_posix() for path in wheel_paths)
|
|
59
|
+
raise typer.BadParameter(
|
|
60
|
+
f"Multiple package wheels found matching {dist_dir}/*.whl: {rendered}. "
|
|
61
|
+
"Pass the intended wheel path explicitly."
|
|
62
|
+
)
|
|
63
|
+
return wheel_paths[0]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_package_wheel_local_path(value: str) -> bool:
|
|
67
|
+
path = Path(value)
|
|
68
|
+
return (
|
|
69
|
+
len(path.parts) >= 2
|
|
70
|
+
and path.parts[0] == "dist"
|
|
71
|
+
and path.suffix.lower() == ".whl"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _remove_tracked_package_wheels(config_data: dict[str, Any]) -> list[str]:
|
|
76
|
+
default_args = config_data.setdefault("DefaultArguments", {})
|
|
77
|
+
sc = config_data.setdefault("SourceControlDetails", {})
|
|
78
|
+
additional_files = sc.get("AdditionalPythonFiles")
|
|
79
|
+
if additional_files is None:
|
|
80
|
+
return []
|
|
81
|
+
if not isinstance(additional_files, list):
|
|
82
|
+
raise typer.BadParameter(
|
|
83
|
+
"SourceControlDetails.AdditionalPythonFiles must be a list."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
remaining_entries: list[dict[str, str]] = []
|
|
87
|
+
removed_s3_paths: list[str] = []
|
|
88
|
+
changes: list[str] = []
|
|
89
|
+
|
|
90
|
+
for entry in additional_files:
|
|
91
|
+
if not isinstance(entry, dict):
|
|
92
|
+
raise typer.BadParameter("AdditionalPythonFiles entries must be objects.")
|
|
93
|
+
local_path = entry.get("LocalPath")
|
|
94
|
+
s3_path = entry.get("S3Path")
|
|
95
|
+
if not local_path or not s3_path:
|
|
96
|
+
raise typer.BadParameter(
|
|
97
|
+
"AdditionalPythonFiles entries must include LocalPath and S3Path."
|
|
98
|
+
)
|
|
99
|
+
if _is_package_wheel_local_path(local_path):
|
|
100
|
+
removed_s3_paths.append(s3_path)
|
|
101
|
+
changes.append(f"Removed package wheel mapping: {local_path}")
|
|
102
|
+
continue
|
|
103
|
+
remaining_entries.append(entry)
|
|
104
|
+
|
|
105
|
+
if len(remaining_entries) != len(additional_files):
|
|
106
|
+
if remaining_entries:
|
|
107
|
+
sc["AdditionalPythonFiles"] = remaining_entries
|
|
108
|
+
else:
|
|
109
|
+
sc.pop("AdditionalPythonFiles", None)
|
|
110
|
+
|
|
111
|
+
updated_modules, removed_modules = _remove_csv_items(
|
|
112
|
+
default_args.get("--additional-python-modules"),
|
|
113
|
+
removed_s3_paths,
|
|
114
|
+
)
|
|
115
|
+
if removed_modules:
|
|
116
|
+
if updated_modules:
|
|
117
|
+
default_args["--additional-python-modules"] = updated_modules
|
|
118
|
+
else:
|
|
119
|
+
default_args.pop("--additional-python-modules", None)
|
|
120
|
+
changes.append(
|
|
121
|
+
f"Removed --additional-python-modules entries: {', '.join(removed_modules)}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
updated_extra_py_files, removed_extra_py_files = _remove_csv_items(
|
|
125
|
+
default_args.get("--extra-py-files"),
|
|
126
|
+
removed_s3_paths,
|
|
127
|
+
)
|
|
128
|
+
if removed_extra_py_files:
|
|
129
|
+
if updated_extra_py_files:
|
|
130
|
+
default_args["--extra-py-files"] = updated_extra_py_files
|
|
131
|
+
else:
|
|
132
|
+
default_args.pop("--extra-py-files", None)
|
|
133
|
+
changes.append(
|
|
134
|
+
f"Removed --extra-py-files entries: {', '.join(removed_extra_py_files)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return changes
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _resolve_glue_add_package_wheel_target(
|
|
141
|
+
args: list[str],
|
|
142
|
+
*,
|
|
143
|
+
job_name: Optional[str],
|
|
144
|
+
config_index: dict[str, dict[str, Any]],
|
|
145
|
+
) -> str:
|
|
146
|
+
if job_name:
|
|
147
|
+
if args:
|
|
148
|
+
raise typer.BadParameter(
|
|
149
|
+
"Do not pass positional args with --job-name and --package-whl."
|
|
150
|
+
)
|
|
151
|
+
return job_name
|
|
152
|
+
|
|
153
|
+
if len(args) == 1 and args[0] in config_index:
|
|
154
|
+
return args[0]
|
|
155
|
+
if args:
|
|
156
|
+
raise typer.BadParameter(
|
|
157
|
+
"With --package-whl, pass at most one positional Glue job name."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return _resolve_single_job_name(None, "glue add --package-whl")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _sync_existing_notebook_config(
|
|
164
|
+
*,
|
|
165
|
+
config_data: dict[str, Any],
|
|
166
|
+
job_name: str,
|
|
167
|
+
dry_run: bool,
|
|
168
|
+
) -> bool:
|
|
169
|
+
sc = config_data.get("SourceControlDetails", {})
|
|
170
|
+
if not isinstance(sc, dict):
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
script_path = Path(
|
|
174
|
+
sc.get("ScriptLocation")
|
|
175
|
+
or sc.get("LocalPath")
|
|
176
|
+
or f"glue/scripts/{slugify(job_name)}.py"
|
|
177
|
+
)
|
|
178
|
+
notebook_value = sc.get("NotebookLocation") or sc.get("NotebookPath")
|
|
179
|
+
notebook_path = (
|
|
180
|
+
Path(notebook_value)
|
|
181
|
+
if isinstance(notebook_value, str)
|
|
182
|
+
else Path(_resolve_notebook_path(script_path))
|
|
183
|
+
)
|
|
184
|
+
if not notebook_path.exists():
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
return _update_notebook_config_cell(
|
|
188
|
+
notebook_path,
|
|
189
|
+
config_data,
|
|
190
|
+
{"--additional-python-modules"},
|
|
191
|
+
dry_run,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _apply_add_mutations(
|
|
196
|
+
*,
|
|
197
|
+
config_data: dict[str, Any],
|
|
198
|
+
job_name: str,
|
|
199
|
+
items: list[str],
|
|
200
|
+
as_path: bool,
|
|
201
|
+
as_pypi: bool,
|
|
202
|
+
) -> list[str]:
|
|
203
|
+
default_args = config_data.setdefault("DefaultArguments", {})
|
|
204
|
+
sc = config_data.setdefault("SourceControlDetails", {})
|
|
205
|
+
|
|
206
|
+
command = config_data.get("Command", {})
|
|
207
|
+
script_location = command.get("ScriptLocation")
|
|
208
|
+
if not script_location:
|
|
209
|
+
raise typer.BadParameter("Missing Command.ScriptLocation in config.")
|
|
210
|
+
local_script_path = Path(
|
|
211
|
+
sc.get("ScriptLocation")
|
|
212
|
+
or sc.get("LocalPath")
|
|
213
|
+
or f"glue/scripts/{slugify(job_name)}.py"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
modules: list[str] = []
|
|
217
|
+
paths: list[Path] = []
|
|
218
|
+
|
|
219
|
+
for item in items:
|
|
220
|
+
if as_path:
|
|
221
|
+
kind = "path"
|
|
222
|
+
elif as_pypi:
|
|
223
|
+
kind = "pypi"
|
|
224
|
+
elif _looks_like_remote_module_spec(item):
|
|
225
|
+
kind = "pypi"
|
|
226
|
+
else:
|
|
227
|
+
looks_like_path = (
|
|
228
|
+
"/" in item
|
|
229
|
+
or "\\" in item
|
|
230
|
+
or item.startswith(".")
|
|
231
|
+
or Path(item).suffix in {".py", ".zip"}
|
|
232
|
+
or Path(item).exists()
|
|
233
|
+
)
|
|
234
|
+
kind = "path" if looks_like_path else "pypi"
|
|
235
|
+
|
|
236
|
+
if kind == "path":
|
|
237
|
+
path = Path(item)
|
|
238
|
+
if path.is_absolute():
|
|
239
|
+
raise typer.BadParameter(f"Local path must be relative: {path}")
|
|
240
|
+
if not path.exists():
|
|
241
|
+
raise typer.BadParameter(f"Local path not found: {path}")
|
|
242
|
+
paths.append(path)
|
|
243
|
+
else:
|
|
244
|
+
modules.append(item.strip())
|
|
245
|
+
|
|
246
|
+
updates: list[str] = []
|
|
247
|
+
|
|
248
|
+
if modules:
|
|
249
|
+
updated = _append_csv_items(
|
|
250
|
+
default_args.get("--additional-python-modules"), modules
|
|
251
|
+
)
|
|
252
|
+
if updated != default_args.get("--additional-python-modules", ""):
|
|
253
|
+
default_args["--additional-python-modules"] = updated
|
|
254
|
+
updates.append(f"Added python modules: {', '.join(modules)}")
|
|
255
|
+
|
|
256
|
+
if paths:
|
|
257
|
+
additional_files = sc.get("AdditionalPythonFiles")
|
|
258
|
+
if additional_files is None:
|
|
259
|
+
additional_files = []
|
|
260
|
+
if not isinstance(additional_files, list):
|
|
261
|
+
raise typer.BadParameter(
|
|
262
|
+
"SourceControlDetails.AdditionalPythonFiles must be a list."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
local_to_entry: dict[str, dict[str, str]] = {}
|
|
266
|
+
s3_to_local: dict[str, str] = {}
|
|
267
|
+
for entry in additional_files:
|
|
268
|
+
if not isinstance(entry, dict):
|
|
269
|
+
raise typer.BadParameter(
|
|
270
|
+
"AdditionalPythonFiles entries must be objects."
|
|
271
|
+
)
|
|
272
|
+
local = entry.get("LocalPath")
|
|
273
|
+
s3_path = entry.get("S3Path")
|
|
274
|
+
if not local or not s3_path:
|
|
275
|
+
raise typer.BadParameter(
|
|
276
|
+
"AdditionalPythonFiles entries must include LocalPath and S3Path."
|
|
277
|
+
)
|
|
278
|
+
if local in local_to_entry:
|
|
279
|
+
raise typer.BadParameter(
|
|
280
|
+
f"Duplicate LocalPath in AdditionalPythonFiles: {local}"
|
|
281
|
+
)
|
|
282
|
+
if s3_path in s3_to_local and s3_to_local[s3_path] != local:
|
|
283
|
+
raise typer.BadParameter(
|
|
284
|
+
f"S3Path {s3_path} already mapped to {s3_to_local[s3_path]}"
|
|
285
|
+
)
|
|
286
|
+
local_to_entry[local] = entry
|
|
287
|
+
s3_to_local[s3_path] = local
|
|
288
|
+
|
|
289
|
+
existing_extra = _parse_csv_list(default_args.get("--extra-py-files"))
|
|
290
|
+
existing_modules = _parse_csv_list(
|
|
291
|
+
default_args.get("--additional-python-modules")
|
|
292
|
+
)
|
|
293
|
+
extra_list = list(existing_extra)
|
|
294
|
+
extra_set = set(existing_extra)
|
|
295
|
+
module_list = list(existing_modules)
|
|
296
|
+
module_set = set(existing_modules)
|
|
297
|
+
|
|
298
|
+
for path in paths:
|
|
299
|
+
s3_path = _derive_s3_target(
|
|
300
|
+
path, job_name, script_location, local_script_path
|
|
301
|
+
)
|
|
302
|
+
if path.is_dir() and not s3_path.endswith(".zip"):
|
|
303
|
+
s3_path = f"{s3_path}.zip"
|
|
304
|
+
use_additional_modules = _routes_to_additional_python_modules(path)
|
|
305
|
+
|
|
306
|
+
local_key = path.as_posix()
|
|
307
|
+
if s3_path in s3_to_local and s3_to_local[s3_path] != local_key:
|
|
308
|
+
raise typer.BadParameter(
|
|
309
|
+
f"S3Path {s3_path} already mapped to {s3_to_local[s3_path]}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
existing_entry = local_to_entry.get(local_key)
|
|
313
|
+
if existing_entry:
|
|
314
|
+
old_s3 = existing_entry.get("S3Path")
|
|
315
|
+
if old_s3 != s3_path:
|
|
316
|
+
existing_entry["S3Path"] = s3_path
|
|
317
|
+
s3_to_local.pop(old_s3, None)
|
|
318
|
+
s3_to_local[s3_path] = local_key
|
|
319
|
+
if old_s3 in extra_set:
|
|
320
|
+
extra_set.remove(old_s3)
|
|
321
|
+
if old_s3 in extra_list:
|
|
322
|
+
extra_list.remove(old_s3)
|
|
323
|
+
updates.append(
|
|
324
|
+
f"Updated python file mapping: {local_key} -> {s3_path}"
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
new_entry = {"LocalPath": local_key, "S3Path": s3_path}
|
|
328
|
+
additional_files.append(new_entry)
|
|
329
|
+
local_to_entry[local_key] = new_entry
|
|
330
|
+
s3_to_local[s3_path] = local_key
|
|
331
|
+
updates.append(f"Added python file mapping: {local_key} -> {s3_path}")
|
|
332
|
+
|
|
333
|
+
if use_additional_modules:
|
|
334
|
+
if s3_path in extra_set:
|
|
335
|
+
extra_set.remove(s3_path)
|
|
336
|
+
if s3_path in extra_list:
|
|
337
|
+
extra_list.remove(s3_path)
|
|
338
|
+
updates.append(f"Removed --extra-py-files entry: {s3_path}")
|
|
339
|
+
if s3_path not in module_set:
|
|
340
|
+
module_set.add(s3_path)
|
|
341
|
+
module_list.append(s3_path)
|
|
342
|
+
updates.append(
|
|
343
|
+
f"Added --additional-python-modules entry: {s3_path}"
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
if s3_path not in extra_set:
|
|
347
|
+
extra_set.add(s3_path)
|
|
348
|
+
extra_list.append(s3_path)
|
|
349
|
+
updates.append(f"Added --extra-py-files entry: {s3_path}")
|
|
350
|
+
|
|
351
|
+
if updates:
|
|
352
|
+
sc["AdditionalPythonFiles"] = additional_files
|
|
353
|
+
if extra_list:
|
|
354
|
+
default_args["--extra-py-files"] = ",".join(extra_list)
|
|
355
|
+
else:
|
|
356
|
+
default_args.pop("--extra-py-files", None)
|
|
357
|
+
if module_list:
|
|
358
|
+
default_args["--additional-python-modules"] = ",".join(module_list)
|
|
359
|
+
elif not modules:
|
|
360
|
+
default_args.pop("--additional-python-modules", None)
|
|
361
|
+
|
|
362
|
+
return updates
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@app.command(
|
|
366
|
+
"add",
|
|
367
|
+
help="Compatibility command for inferred artifact and module additions. Prefer edit for explicit config changes.",
|
|
368
|
+
epilog=_examples_epilog(
|
|
369
|
+
"gluekit checkout my-job",
|
|
370
|
+
"gluekit add dist/my-package.whl",
|
|
371
|
+
"gluekit add --job-name other-job requests==2.32.3 --as-pypi",
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
def glue_add(
|
|
375
|
+
args: Optional[list[str]] = typer.Argument(
|
|
376
|
+
None,
|
|
377
|
+
help="Paths or module specifiers to add to the selected Glue job config.",
|
|
378
|
+
),
|
|
379
|
+
job_name: Optional[str] = typer.Option(
|
|
380
|
+
None,
|
|
381
|
+
"--job-name",
|
|
382
|
+
help="Override the active checked-out job for this command only.",
|
|
383
|
+
),
|
|
384
|
+
as_path: bool = typer.Option(
|
|
385
|
+
False,
|
|
386
|
+
"--as-path",
|
|
387
|
+
help="Treat all items as local paths.",
|
|
388
|
+
),
|
|
389
|
+
as_pypi: bool = typer.Option(
|
|
390
|
+
False,
|
|
391
|
+
"--as-pypi",
|
|
392
|
+
help="Treat all items as PyPI modules.",
|
|
393
|
+
),
|
|
394
|
+
package_whl: bool = typer.Option(
|
|
395
|
+
False,
|
|
396
|
+
"--package-whl",
|
|
397
|
+
help="Use the package wheel from dist/*.whl.",
|
|
398
|
+
),
|
|
399
|
+
dry_run: bool = typer.Option(
|
|
400
|
+
False,
|
|
401
|
+
"--dry-run",
|
|
402
|
+
help="Show what would be updated without writing files.",
|
|
403
|
+
),
|
|
404
|
+
config_dir: Path = typer.Option(
|
|
405
|
+
Path("glue/configs"),
|
|
406
|
+
"--config-dir",
|
|
407
|
+
help="Directory containing Glue job config files.",
|
|
408
|
+
),
|
|
409
|
+
) -> None:
|
|
410
|
+
"""Compatibility command for inferred artifact and module additions."""
|
|
411
|
+
if as_path and as_pypi:
|
|
412
|
+
raise typer.BadParameter("Use only one of --as-path or --as-pypi.")
|
|
413
|
+
|
|
414
|
+
config_index = _load_config_index(config_dir)
|
|
415
|
+
if package_whl:
|
|
416
|
+
job_name = _resolve_glue_add_package_wheel_target(
|
|
417
|
+
args or [],
|
|
418
|
+
job_name=job_name,
|
|
419
|
+
config_index=config_index,
|
|
420
|
+
)
|
|
421
|
+
items = [_resolve_package_wheel_path().as_posix()]
|
|
422
|
+
as_path = True
|
|
423
|
+
else:
|
|
424
|
+
job_name, items = _resolve_glue_add_target(args or [], job_name=job_name)
|
|
425
|
+
if not items:
|
|
426
|
+
raise typer.BadParameter("Provide one or more paths or PyPI modules.")
|
|
427
|
+
|
|
428
|
+
config_entry = config_index.get(job_name)
|
|
429
|
+
if not config_entry:
|
|
430
|
+
if job_name in _get_checked_out_jobs():
|
|
431
|
+
_raise_missing_local_config(job_name, config_dir, "glue add")
|
|
432
|
+
raise typer.BadParameter(
|
|
433
|
+
f'No config files matched "{job_name}" in {config_dir}.'
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
config_path: Path = config_entry["config_path"]
|
|
437
|
+
config_data = config_entry["config"]
|
|
438
|
+
_emit_compatibility_notice("add", "when you want explicit field-level mutations")
|
|
439
|
+
removal_updates = []
|
|
440
|
+
if package_whl:
|
|
441
|
+
removal_updates = _remove_tracked_package_wheels(config_data)
|
|
442
|
+
updates = removal_updates + _apply_add_mutations(
|
|
443
|
+
config_data=config_data,
|
|
444
|
+
job_name=job_name,
|
|
445
|
+
items=items,
|
|
446
|
+
as_path=as_path,
|
|
447
|
+
as_pypi=as_pypi,
|
|
448
|
+
)
|
|
449
|
+
_write_config_changes(config_path, config_data, updates, dry_run=dry_run)
|
|
450
|
+
if updates:
|
|
451
|
+
_sync_existing_notebook_config(
|
|
452
|
+
config_data=config_data,
|
|
453
|
+
job_name=job_name,
|
|
454
|
+
dry_run=dry_run,
|
|
455
|
+
)
|