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.
Files changed (46) hide show
  1. gluekit/__init__.py +7 -0
  2. gluekit/app.py +0 -0
  3. gluekit/cli.py +64 -0
  4. gluekit/commands/__init__.py +1 -0
  5. gluekit/commands/add.py +455 -0
  6. gluekit/commands/build.py +816 -0
  7. gluekit/commands/checkout.py +114 -0
  8. gluekit/commands/clone.py +516 -0
  9. gluekit/commands/config_commands.py +180 -0
  10. gluekit/commands/constants.py +47 -0
  11. gluekit/commands/convert.py +336 -0
  12. gluekit/commands/edit.py +1104 -0
  13. gluekit/commands/helpers.py +1068 -0
  14. gluekit/commands/init.py +798 -0
  15. gluekit/commands/list.py +16 -0
  16. gluekit/commands/local_commands.py +680 -0
  17. gluekit/commands/pull.py +374 -0
  18. gluekit/commands/push.py +251 -0
  19. gluekit/commands/remove.py +161 -0
  20. gluekit/commands/run.py +126 -0
  21. gluekit/commands/status.py +97 -0
  22. gluekit/commands/sync.py +97 -0
  23. gluekit/commands/update.py +104 -0
  24. gluekit/job_mgmt/__init__.py +0 -0
  25. gluekit/job_mgmt/glue_jobs.py +1323 -0
  26. gluekit/job_mgmt/magics.py +122 -0
  27. gluekit/job_mgmt/resources/__init__.py +0 -0
  28. gluekit/job_mgmt/resources/glue_job_schema.json +40341 -0
  29. gluekit/job_mgmt/resources/magic_map.json +83 -0
  30. gluekit/job_mgmt/schema.py +165 -0
  31. gluekit/local/__init__.py +6 -0
  32. gluekit/local/awsglue/__init__.py +1 -0
  33. gluekit/local/awsglue/context.py +30 -0
  34. gluekit/local/awsglue/job.py +9 -0
  35. gluekit/local/awsglue/utils.py +17 -0
  36. gluekit/local/local.py +434 -0
  37. gluekit/local/local_fixtures.py +337 -0
  38. gluekit/local/pyspark/__init__.py +7 -0
  39. gluekit/local/pyspark/context.py +31 -0
  40. gluekit/local/pyspark/sql/__init__.py +6 -0
  41. gluekit/local/pyspark/sql/session.py +29 -0
  42. gluekit-1.0.1.dev1.dist-info/METADATA +1176 -0
  43. gluekit-1.0.1.dev1.dist-info/RECORD +46 -0
  44. gluekit-1.0.1.dev1.dist-info/WHEEL +5 -0
  45. gluekit-1.0.1.dev1.dist-info/entry_points.txt +2 -0
  46. gluekit-1.0.1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import copy
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import tarfile
10
+ import uuid
11
+ import zipfile
12
+ from collections.abc import Mapping
13
+ from datetime import datetime, timezone
14
+ from email.parser import Parser
15
+ from fnmatch import fnmatch
16
+ from pathlib import Path
17
+ from tempfile import TemporaryDirectory
18
+ from typing import Any, Optional
19
+
20
+ import typer
21
+ from slugify import slugify
22
+
23
+ from ..job_mgmt.glue_jobs import (
24
+ download_glue_job_files,
25
+ list_glue_jobs,
26
+ normalize_glue_config_data,
27
+ convert_script_to_notebook,
28
+ convert_notebook_to_script,
29
+ _resolve_notebook_path,
30
+ upload_glue_job_files_from_config,
31
+ )
32
+ from ..job_mgmt.magics import build_magic_cell_sources as _build_magic_cell_sources
33
+
34
+ from .constants import *
35
+ from .helpers import *
36
+ from ..cli import app, glue_config_app
37
+
38
+
39
+ def _remove_local_path(path: Path) -> bool:
40
+ if not path.exists():
41
+ return False
42
+ if path.is_dir():
43
+ shutil.rmtree(path)
44
+ else:
45
+ path.unlink()
46
+ return True
47
+
48
+
49
+ @app.command(
50
+ "remove",
51
+ epilog=_examples_epilog(
52
+ "gluekit remove my-job",
53
+ "gluekit remove my-job --remove-additional-python-files",
54
+ 'gluekit remove "my-job-\\*" --remove-extra-files --dry-run',
55
+ ),
56
+ )
57
+ def glue_remove(
58
+ job_name: Optional[str] = typer.Argument(
59
+ None,
60
+ help='Local Glue job name or pattern to remove. Use "*" for all local configs.',
61
+ ),
62
+ dry_run: bool = typer.Option(
63
+ False,
64
+ "--dry-run",
65
+ help="Show what would be removed without deleting files.",
66
+ ),
67
+ remove_additional_python_files: bool = typer.Option(
68
+ False,
69
+ "--remove-additional-python-files",
70
+ help="Also remove local files or directories referenced by SourceControlDetails.AdditionalPythonFiles.",
71
+ ),
72
+ remove_extra_files: bool = typer.Option(
73
+ False,
74
+ "--remove-extra-files",
75
+ help="Also remove local files or directories referenced by SourceControlDetails.AdditionalFiles and ExtraFiles.",
76
+ ),
77
+ config_dir: Path = typer.Option(
78
+ Path("glue/configs"),
79
+ "--config-dir",
80
+ help="Directory containing Glue job config files.",
81
+ ),
82
+ ) -> None:
83
+ """Remove local Glue job configs and local artifacts."""
84
+ config_index = _load_config_index(config_dir)
85
+ if not config_index:
86
+ raise typer.BadParameter(f"No config files found in {config_dir}")
87
+
88
+ checked_out_jobs = _get_checked_out_jobs()
89
+ if job_name is None:
90
+ if not checked_out_jobs:
91
+ raise typer.BadParameter(
92
+ "Provide <job-name> or run 'gluekit checkout <job-name>' before glue remove."
93
+ )
94
+ selected_entries = [
95
+ (name, entry)
96
+ for name, entry in config_index.items()
97
+ if name in checked_out_jobs
98
+ ]
99
+ if not selected_entries:
100
+ missing_job_names = ", ".join(sorted(checked_out_jobs))
101
+ raise typer.BadParameter(
102
+ f"No local config files were found for the active checkout selection: {missing_job_names}."
103
+ )
104
+ typer.echo(
105
+ f"Using active checkout selection ({len(selected_entries)} config(s))."
106
+ )
107
+ elif job_name == "*" and checked_out_jobs:
108
+ selected_entries = [
109
+ (name, entry)
110
+ for name, entry in config_index.items()
111
+ if name in checked_out_jobs
112
+ ]
113
+ if not selected_entries:
114
+ missing_job_names = ", ".join(sorted(checked_out_jobs))
115
+ raise typer.BadParameter(
116
+ f"No local config files were found for the active checkout selection: {missing_job_names}."
117
+ )
118
+ typer.echo(
119
+ f"Using active checkout selection ({len(selected_entries)} config(s))."
120
+ )
121
+ elif job_name == "*":
122
+ selected_entries = list(config_index.items())
123
+ else:
124
+ selected_entries = [
125
+ (name, entry)
126
+ for name, entry in config_index.items()
127
+ if fnmatch(name, job_name)
128
+ ]
129
+ if selected_entries:
130
+ typer.echo(
131
+ f'Found {len(selected_entries)} config(s) matching "{job_name}".'
132
+ )
133
+ else:
134
+ _raise_missing_local_config(job_name, config_dir, "glue remove")
135
+
136
+ for name, entry in selected_entries:
137
+ config_path: Path = entry["config_path"]
138
+ config_data = entry.get("config", {})
139
+ artifact_paths = _collect_config_local_artifact_paths(
140
+ config_data,
141
+ job_name=name,
142
+ include_additional_python_files=remove_additional_python_files,
143
+ include_extra_files=remove_extra_files,
144
+ )
145
+
146
+ seen_paths = {config_path.resolve(): ("config", config_path)}
147
+ for kind, path in artifact_paths:
148
+ resolved_path = path.resolve()
149
+ if resolved_path not in seen_paths:
150
+ seen_paths[resolved_path] = (kind, path)
151
+
152
+ if dry_run:
153
+ for kind, path in seen_paths.values():
154
+ typer.echo(f"Would remove {kind}: {path}")
155
+ continue
156
+
157
+ for kind, path in seen_paths.values():
158
+ if _remove_local_path(path):
159
+ typer.echo(f"Removed {kind}: {path}")
160
+ else:
161
+ typer.echo(f"Skipped missing {kind}: {path}")
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+
8
+ from ..cli import app
9
+ from ..local import run_glue_script
10
+ from ..local.local_fixtures import LOCAL_RUN_CONFIG_FILE
11
+ from .helpers import _get_checked_out_local_setup
12
+ from .helpers import _examples_epilog
13
+
14
+
15
+ def _parse_key_value(raw: str, *, option_name: str) -> tuple[str, str]:
16
+ key, separator, value = raw.partition("=")
17
+ if not separator:
18
+ raise typer.BadParameter(
19
+ f"{option_name} entries must be KEY=VALUE. Received: {raw!r}"
20
+ )
21
+ key = key.strip()
22
+ if not key:
23
+ raise typer.BadParameter(
24
+ f"{option_name} entries must include a non-empty key. Received: {raw!r}"
25
+ )
26
+ return key, value
27
+
28
+
29
+ @app.command(
30
+ "run",
31
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
32
+ epilog=_examples_epilog(
33
+ "gluekit run ../ppsc-survey/glue/scripts/job_a.py --create-bucket my-input-bucket",
34
+ "gluekit run glue/scripts/job-a.py --ssm-parameter /app/env=dev --glue-arg report_date=2026-06-18",
35
+ "gluekit run glue/scripts/job-a.py -- --JOB_NAME custom-local-job --report_date 2026-06-18",
36
+ ),
37
+ )
38
+ def glue_run(
39
+ ctx: typer.Context,
40
+ script_path: Path = typer.Argument(
41
+ ...,
42
+ exists=True,
43
+ file_okay=True,
44
+ dir_okay=False,
45
+ readable=True,
46
+ resolve_path=True,
47
+ help="Path to the Glue Python script to execute locally.",
48
+ ),
49
+ glue_arg: Optional[list[str]] = typer.Option(
50
+ None,
51
+ "--glue-arg",
52
+ help="Glue script argument in KEY=VALUE form; passed through as --KEY VALUE.",
53
+ ),
54
+ job_name: str = typer.Option(
55
+ "local-glue-job",
56
+ "--job-name",
57
+ help="Default JOB_NAME used when not provided in script args.",
58
+ ),
59
+ create_bucket: Optional[list[str]] = typer.Option(
60
+ None,
61
+ "--create-bucket",
62
+ help="Create a mocked S3 bucket only inside the local run. Repeat as needed.",
63
+ ),
64
+ ssm_parameter: Optional[list[str]] = typer.Option(
65
+ None,
66
+ "--ssm-parameter",
67
+ help="Seed a mocked SSM parameter only inside the local run as NAME=VALUE. Repeat as needed.",
68
+ ),
69
+ aws_region: Optional[str] = typer.Option(
70
+ None,
71
+ "--aws-region",
72
+ help="Mock AWS region used for local boto3 clients. Defaults to us-east-1.",
73
+ ),
74
+ config_file: Optional[Path] = typer.Option(
75
+ None,
76
+ "--config-file",
77
+ exists=False,
78
+ file_okay=True,
79
+ dir_okay=False,
80
+ resolve_path=True,
81
+ help="Local fixture config file for mocked S3/SSM. Defaults to .gluekit/local.json when local settings are checked out.",
82
+ ),
83
+ ) -> None:
84
+ """Run a Glue script locally with emulated Glue libraries and mocked AWS services."""
85
+ # Translate CLI --glue-arg KEY=VALUE list into a dict for run_glue_script.
86
+ glue_args: dict[str, str] = {}
87
+ for item in glue_arg or []:
88
+ key, value = _parse_key_value(item, option_name="--glue-arg")
89
+ glue_args[key] = value
90
+
91
+ # Translate --ssm-parameter NAME=VALUE list into a dict.
92
+ ssm_parameters: dict[str, str] = {}
93
+ for entry in ssm_parameter or []:
94
+ name, value = _parse_key_value(entry, option_name="--ssm-parameter")
95
+ ssm_parameters[name] = value
96
+
97
+ # Validate bucket names up-front so the error is a clean typer message.
98
+ buckets: list[str] = []
99
+ for bucket_name in create_bucket or []:
100
+ normalized = bucket_name.strip()
101
+ if not normalized:
102
+ raise typer.BadParameter(
103
+ "--create-bucket entries must be non-empty bucket names."
104
+ )
105
+ buckets.append(normalized)
106
+
107
+ local_setup = _get_checked_out_local_setup()
108
+ default_config_file: Path | None = None
109
+ if config_file is not None:
110
+ default_config_file = config_file
111
+ elif isinstance(local_setup, dict):
112
+ default_config_file = LOCAL_RUN_CONFIG_FILE
113
+
114
+ try:
115
+ run_glue_script(
116
+ script_path,
117
+ script_args=list(ctx.args),
118
+ glue_args=glue_args or None,
119
+ job_name=job_name,
120
+ create_buckets=buckets or None,
121
+ ssm_parameters=ssm_parameters or None,
122
+ aws_region=aws_region,
123
+ config_file=default_config_file,
124
+ )
125
+ except (ImportError, ValueError) as exc:
126
+ raise typer.BadParameter(str(exc)) from exc
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from ..cli import app
9
+ from ..job_mgmt.glue_jobs import resolve_glue_artifact_mappings
10
+ from .helpers import (
11
+ _examples_epilog,
12
+ _get_checked_out_jobs,
13
+ _get_checked_out_profile,
14
+ _load_config_index,
15
+ )
16
+
17
+
18
+ def _relative_display_path(path: Path) -> str:
19
+ try:
20
+ return path.resolve().relative_to(Path.cwd().resolve()).as_posix()
21
+ except ValueError:
22
+ return path.as_posix()
23
+
24
+
25
+ def _print_available_jobs(config_index: dict[str, dict]) -> None:
26
+ if not config_index:
27
+ typer.echo("- (none)")
28
+ return
29
+ for job_name in sorted(config_index):
30
+ config_path = config_index[job_name]["config_path"]
31
+ typer.echo(f"- {job_name}: {_relative_display_path(config_path)}")
32
+
33
+
34
+ def _print_pretty_config(config_data: dict) -> None:
35
+ typer.echo("Checked out config:")
36
+ typer.echo(json.dumps(config_data, indent=4))
37
+
38
+
39
+ @app.command(
40
+ "status",
41
+ epilog=_examples_epilog(
42
+ "gluekit status",
43
+ "gluekit status --config-dir glue/configs",
44
+ ),
45
+ )
46
+ def glue_status(
47
+ config_dir: Path = typer.Option(
48
+ Path("glue/configs"),
49
+ "--config-dir",
50
+ help="Directory containing Glue job config files.",
51
+ ),
52
+ ) -> None:
53
+ """Show the active local checkout and its local-to-S3 artifact mappings."""
54
+ config_index = _load_config_index(config_dir)
55
+ checked_out_jobs = _get_checked_out_jobs()
56
+ checked_out_profile = _get_checked_out_profile()
57
+
58
+ if not checked_out_jobs:
59
+ typer.echo("No jobs checked out, available jobs are:")
60
+ _print_available_jobs(config_index)
61
+ return
62
+
63
+ missing_jobs = [
64
+ job_name for job_name in checked_out_jobs if job_name not in config_index
65
+ ]
66
+ if missing_jobs:
67
+ for job_name in missing_jobs:
68
+ typer.echo(f"Checked out job: {job_name}")
69
+ typer.echo(f"Missing local config in: {_relative_display_path(config_dir)}")
70
+ typer.echo("Available jobs are:")
71
+ _print_available_jobs(config_index)
72
+ raise typer.Exit(code=1)
73
+
74
+ for index, job_name in enumerate(checked_out_jobs):
75
+ if index:
76
+ typer.echo()
77
+ entry = config_index[job_name]
78
+ config_path = entry["config_path"]
79
+ resolution = resolve_glue_artifact_mappings(entry["config"])
80
+
81
+ typer.echo(f"Checked out job: {job_name}")
82
+ typer.echo(f"Profile: {checked_out_profile or '(none)'}")
83
+ typer.echo(f"Config: {_relative_display_path(config_path)}")
84
+ _print_pretty_config(entry["config"])
85
+ typer.echo("Artifacts:")
86
+ for mapping in resolution["mappings"]:
87
+ local_path = Path(mapping["local_path"])
88
+ local_status = "present" if local_path.exists() else "missing"
89
+ typer.echo(
90
+ f"- {mapping['artifact_type']}: "
91
+ f"{mapping['local_path']} -> {mapping['s3_path']} ({local_status})"
92
+ )
93
+ if not resolution["mappings"]:
94
+ typer.echo("- (none)")
95
+
96
+ for warning in resolution["warnings"]:
97
+ typer.echo(f"Warning: {warning}", err=True)
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import copy
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import tarfile
10
+ import uuid
11
+ import zipfile
12
+ from collections.abc import Mapping
13
+ from datetime import datetime, timezone
14
+ from email.parser import Parser
15
+ from fnmatch import fnmatch
16
+ from pathlib import Path
17
+ from tempfile import TemporaryDirectory
18
+ from typing import Any, Optional
19
+
20
+ import typer
21
+ from slugify import slugify
22
+
23
+ from ..job_mgmt.glue_jobs import (
24
+ download_glue_job_files,
25
+ list_glue_jobs,
26
+ normalize_glue_config_data,
27
+ convert_script_to_notebook,
28
+ convert_notebook_to_script,
29
+ _resolve_notebook_path,
30
+ upload_glue_job_files_from_config,
31
+ )
32
+ from ..job_mgmt.magics import build_magic_cell_sources as _build_magic_cell_sources
33
+
34
+ from .constants import *
35
+ from .helpers import *
36
+ from .convert import _update_script_config_cell, _update_notebook_config_cell
37
+ from ..cli import app, glue_config_app
38
+
39
+
40
+ @app.command(
41
+ "sync",
42
+ epilog=_examples_epilog(
43
+ "gluekit sync my-job",
44
+ "gluekit sync my-job --dry-run",
45
+ ),
46
+ )
47
+ def glue_sync(
48
+ job_name: Optional[str] = typer.Argument(
49
+ None,
50
+ help="Glue job name to sync. Defaults to the active checkout selection.",
51
+ ),
52
+ dry_run: bool = typer.Option(
53
+ False,
54
+ "--dry-run",
55
+ help="Show what would be updated without writing files.",
56
+ ),
57
+ config_dir: Path = typer.Option(
58
+ Path("glue/configs"),
59
+ "--config-dir",
60
+ help="Directory containing Glue job config files.",
61
+ ),
62
+ ) -> None:
63
+ """Sync generated Glue config metadata cells in local script/notebook files."""
64
+ job_name = _resolve_single_job_name(job_name, "glue sync")
65
+ config_index = _load_config_index(config_dir)
66
+ config_entry = config_index.get(job_name)
67
+ if not config_entry:
68
+ _raise_missing_local_config(job_name, config_dir, "glue sync")
69
+
70
+ config_data = config_entry.get("config", {})
71
+ sc = config_data.get("SourceControlDetails", {})
72
+ script_path = Path(
73
+ sc.get("ScriptLocation")
74
+ or sc.get("LocalPath")
75
+ or f"glue/scripts/{slugify(job_name)}.py"
76
+ )
77
+ notebook_path_value = sc.get("NotebookLocation") or sc.get("NotebookPath")
78
+ if notebook_path_value:
79
+ notebook_path = Path(notebook_path_value)
80
+ else:
81
+ notebook_path = Path(_resolve_notebook_path(script_path))
82
+
83
+ updated_any = _update_script_config_cell(script_path, config_data, dry_run)
84
+
85
+ if config_data.get("JobMode") == "NOTEBOOK":
86
+ updated_any = (
87
+ _update_notebook_config_cell(
88
+ notebook_path,
89
+ config_data,
90
+ {"--additional-python-modules", "--extra-py-files"},
91
+ dry_run,
92
+ )
93
+ or updated_any
94
+ )
95
+
96
+ if not updated_any:
97
+ typer.echo("No sync updates needed.")
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import copy
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import tarfile
10
+ import uuid
11
+ import zipfile
12
+ from collections.abc import Mapping
13
+ from datetime import datetime, timezone
14
+ from email.parser import Parser
15
+ from fnmatch import fnmatch
16
+ from pathlib import Path
17
+ from tempfile import TemporaryDirectory
18
+ from typing import Any, Optional
19
+
20
+ import typer
21
+ from slugify import slugify
22
+
23
+ from ..job_mgmt.glue_jobs import (
24
+ download_glue_job_files,
25
+ list_glue_jobs,
26
+ normalize_glue_config_data,
27
+ convert_script_to_notebook,
28
+ convert_notebook_to_script,
29
+ _resolve_notebook_path,
30
+ upload_glue_job_files_from_config,
31
+ )
32
+ from ..job_mgmt.magics import build_magic_cell_sources as _build_magic_cell_sources
33
+
34
+ from .constants import *
35
+ from .helpers import *
36
+ from .edit import _apply_edit_mutations
37
+ from ..cli import app, glue_config_app
38
+ from .edit import _apply_edit_mutations
39
+
40
+
41
+ def _resolve_glue_update_target(
42
+ args: list[str],
43
+ *,
44
+ config_dir: Path,
45
+ ) -> tuple[str, str, str]:
46
+ if len(args) == 3:
47
+ config_index = _load_config_index(config_dir)
48
+ if args[0] in config_index:
49
+ return args[0], args[1], args[2]
50
+
51
+ if len(args) == 2:
52
+ job_name = _resolve_single_job_name(None, "glue update")
53
+ return job_name, args[0], args[1]
54
+
55
+ raise typer.BadParameter(
56
+ "Use 'gluekit update <job-name> <property> <value>' or, with one checked-out job, 'gluekit update <property> <value>'."
57
+ )
58
+
59
+
60
+ @app.command(
61
+ "update",
62
+ help="Compatibility command for generic property updates. Prefer edit for explicit config changes.",
63
+ epilog=_examples_epilog(
64
+ "gluekit update my-job Description 'Updated job description'",
65
+ "gluekit update my-job command.ScriptLocation s3://my-bucket/scripts/my-job.py",
66
+ "gluekit update default_arguments.--TempDir s3://my-bucket/tmp/",
67
+ ),
68
+ )
69
+ def glue_update(
70
+ args: list[str] = typer.Argument(
71
+ ...,
72
+ help="Either <job-name> <property> <value> or, with the active checkout selection, just <property> <value>.",
73
+ ),
74
+ dry_run: bool = typer.Option(
75
+ False,
76
+ "--dry-run",
77
+ help="Show what would be updated without writing files.",
78
+ ),
79
+ config_dir: Path = typer.Option(
80
+ Path("glue/configs"),
81
+ "--config-dir",
82
+ help="Directory containing Glue job config files.",
83
+ ),
84
+ ) -> None:
85
+ """Compatibility command for generic property updates."""
86
+ job_name, property_name, raw_value = _resolve_glue_update_target(
87
+ args, config_dir=config_dir
88
+ )
89
+ if not property_name.strip():
90
+ raise typer.BadParameter("Property name must be a non-empty string.")
91
+
92
+ config_index = _load_config_index(config_dir)
93
+ config_entry = config_index.get(job_name)
94
+ if not config_entry:
95
+ _raise_missing_local_config(job_name, config_dir, "glue update")
96
+
97
+ config_path: Path = config_entry["config_path"]
98
+ config_data = config_entry["config"]
99
+ _emit_compatibility_notice("update", "for named field flags and list operations")
100
+ changes = _apply_saved_params_to_config(
101
+ config_data,
102
+ {property_name: _coerce_set_value(raw_value)},
103
+ )
104
+ _write_config_changes(config_path, config_data, changes, dry_run=dry_run)
File without changes