Skip to content

Commit 88ab113

Browse files
authored
Merge pull request #56 from Integration-Automation/dev
Address SonarCloud open issues
2 parents cd1f1e4 + dcd5914 commit 88ab113

39 files changed

+1048
-379
lines changed

automation_file/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,7 @@
236236
from automation_file.utils.rotate import RotateException, rotate_backups
237237

238238
if TYPE_CHECKING:
239-
from automation_file.ui.launcher import (
240-
launch_ui as launch_ui, # pylint: disable=useless-import-alias
241-
)
239+
from automation_file.ui.launcher import launch_ui
242240

243241
# Shared callback executor + package loader wired to the shared registry.
244242
callback_executor: CallbackExecutor = CallbackExecutor(executor.registry)

automation_file/__main__.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,7 @@ def _sleep_forever() -> None:
133133
time.sleep(3600)
134134

135135

136-
def _build_parser() -> argparse.ArgumentParser:
137-
parser = argparse.ArgumentParser(prog="automation_file")
138-
parser.add_argument("-e", "--execute_file", help="path to an action JSON file")
139-
parser.add_argument("-d", "--execute_dir", help="directory containing action JSON files")
140-
parser.add_argument("-c", "--create_project", help="scaffold a project at this path")
141-
parser.add_argument("--execute_str", help="JSON action list as a string")
142-
143-
subparsers = parser.add_subparsers(dest="command")
144-
136+
def _add_zip_commands(subparsers: argparse._SubParsersAction) -> None:
145137
zip_parser = subparsers.add_parser("zip", help="zip a file or directory")
146138
zip_parser.add_argument("source")
147139
zip_parser.add_argument("target")
@@ -159,6 +151,8 @@ def _build_parser() -> argparse.ArgumentParser:
159151
unzip_parser.add_argument("--password", default=None)
160152
unzip_parser.set_defaults(handler=_cmd_unzip)
161153

154+
155+
def _add_file_commands(subparsers: argparse._SubParsersAction) -> None:
162156
download_parser = subparsers.add_parser("download", help="SSRF-validated HTTP download")
163157
download_parser.add_argument("url")
164158
download_parser.add_argument("output")
@@ -169,6 +163,8 @@ def _build_parser() -> argparse.ArgumentParser:
169163
touch_parser.add_argument("--content", default="")
170164
touch_parser.set_defaults(handler=_cmd_create_file)
171165

166+
167+
def _add_server_commands(subparsers: argparse._SubParsersAction) -> None:
172168
server_parser = subparsers.add_parser("server", help="run the TCP action server")
173169
server_parser.add_argument("--host", default="localhost")
174170
server_parser.add_argument("--port", type=int, default=9943)
@@ -183,6 +179,8 @@ def _build_parser() -> argparse.ArgumentParser:
183179
http_parser.add_argument("--shared-secret", default=None)
184180
http_parser.set_defaults(handler=_cmd_http_server)
185181

182+
183+
def _add_integration_commands(subparsers: argparse._SubParsersAction) -> None:
186184
ui_parser = subparsers.add_parser("ui", help="launch the PySide6 GUI")
187185
ui_parser.set_defaults(handler=_cmd_ui)
188186

@@ -206,6 +204,19 @@ def _build_parser() -> argparse.ArgumentParser:
206204
drive_parser.add_argument("--name", default=None)
207205
drive_parser.set_defaults(handler=_cmd_drive_upload)
208206

207+
208+
def _build_parser() -> argparse.ArgumentParser:
209+
parser = argparse.ArgumentParser(prog="automation_file")
210+
parser.add_argument("-e", "--execute_file", help="path to an action JSON file")
211+
parser.add_argument("-d", "--execute_dir", help="directory containing action JSON files")
212+
parser.add_argument("-c", "--create_project", help="scaffold a project at this path")
213+
parser.add_argument("--execute_str", help="JSON action list as a string")
214+
215+
subparsers = parser.add_subparsers(dest="command")
216+
_add_zip_commands(subparsers)
217+
_add_file_commands(subparsers)
218+
_add_server_commands(subparsers)
219+
_add_integration_commands(subparsers)
209220
return parser
210221

211222

automation_file/core/config.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
from automation_file.notify.manager import NotificationManager
5454
from automation_file.notify.sinks import (
5555
EmailSink,
56-
NotificationException,
5756
NotificationSink,
5857
SlackSink,
5958
WebhookSink,
@@ -156,8 +155,6 @@ def _build_sink(entry: dict[str, Any]) -> NotificationSink:
156155
)
157156
try:
158157
return builder(entry)
159-
except NotificationException:
160-
raise
161158
except (TypeError, ValueError) as err:
162159
raise ConfigException(
163160
f"invalid config for sink {entry.get('name') or sink_type!r}: {err}"

automation_file/core/dag_executor.py

Lines changed: 76 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,75 @@
3131
__all__ = ["execute_action_dag"]
3232

3333

34+
class _DagRun:
35+
"""Mutable scheduling state shared by the submit / completion helpers."""
36+
37+
def __init__(
38+
self,
39+
nodes: list[Mapping[str, Any]],
40+
pool: ThreadPoolExecutor,
41+
fail_fast: bool,
42+
) -> None:
43+
self.graph, self.indegree = _build_graph(nodes)
44+
self.node_map = {_require_id(node): node for node in nodes}
45+
self.results: dict[str, Any] = {}
46+
self.lock = threading.Lock()
47+
self.ready: deque[str] = deque(
48+
node_id for node_id, count in self.indegree.items() if count == 0
49+
)
50+
self.in_flight: dict[Future[Any], str] = {}
51+
self.pool = pool
52+
self.fail_fast = fail_fast
53+
54+
def _mark_skipped(self, dependent: str, reason_id: str) -> None:
55+
with self.lock:
56+
if dependent in self.results:
57+
return
58+
self.results[dependent] = f"skipped: dep {reason_id!r} failed"
59+
for grandchild in self.graph.get(dependent, ()):
60+
self.indegree[grandchild] -= 1
61+
self._mark_skipped(grandchild, dependent)
62+
63+
def _skip_dependents(self, node_id: str) -> None:
64+
for dependent in self.graph.get(node_id, ()):
65+
self.indegree[dependent] -= 1
66+
self._mark_skipped(dependent, node_id)
67+
68+
def submit(self, node_id: str) -> None:
69+
action = self.node_map[node_id].get("action")
70+
if not isinstance(action, list):
71+
err = DagException(f"node {node_id!r} missing action list")
72+
with self.lock:
73+
self.results[node_id] = repr(err)
74+
if self.fail_fast:
75+
self._skip_dependents(node_id)
76+
return
77+
future = self.pool.submit(_run_action, action)
78+
self.in_flight[future] = node_id
79+
80+
def _complete(self, node_id: str, value: Any, failed: bool) -> None:
81+
with self.lock:
82+
self.results[node_id] = value
83+
for dependent in self.graph.get(node_id, ()):
84+
self.indegree[dependent] -= 1
85+
if failed and self.fail_fast:
86+
self._mark_skipped(dependent, node_id)
87+
elif self.indegree[dependent] == 0 and dependent not in self.results:
88+
self.ready.append(dependent)
89+
90+
def drain_completed(self) -> None:
91+
done, _ = wait(list(self.in_flight), return_when=FIRST_COMPLETED)
92+
for future in done:
93+
node_id = self.in_flight.pop(future)
94+
try:
95+
value: Any = future.result()
96+
failed = False
97+
except Exception as err: # pylint: disable=broad-except
98+
value = repr(err)
99+
failed = True
100+
self._complete(node_id, value, failed)
101+
102+
34103
def execute_action_dag(
35104
nodes: list[Mapping[str, Any]],
36105
max_workers: int = 4,
@@ -46,54 +115,15 @@ def execute_action_dag(
46115
Raises :class:`DagException` for static errors detected before any action
47116
runs: duplicate ids, unknown dependencies, or cycles.
48117
"""
49-
graph, indegree = _build_graph(nodes)
50-
node_map = {_require_id(node): node for node in nodes}
51-
results: dict[str, Any] = {}
52-
lock = threading.Lock()
53-
54-
ready: deque[str] = deque(node_id for node_id, count in indegree.items() if count == 0)
55-
56118
with ThreadPoolExecutor(max_workers=max_workers) as pool:
57-
in_flight: dict[Future[Any], str] = {}
58-
59-
def submit(node_id: str) -> None:
60-
action = node_map[node_id].get("action")
61-
if not isinstance(action, list):
62-
err = DagException(f"node {node_id!r} missing action list")
63-
with lock:
64-
results[node_id] = repr(err)
65-
if fail_fast:
66-
for dependent in graph.get(node_id, ()):
67-
indegree[dependent] -= 1
68-
_mark_skipped(dependent, node_id, graph, indegree, results, lock)
69-
return
70-
future = pool.submit(_run_action, action)
71-
in_flight[future] = node_id
72-
73-
while ready or in_flight:
74-
while ready:
75-
submit(ready.popleft())
76-
if not in_flight:
119+
state = _DagRun(nodes, pool, fail_fast)
120+
while state.ready or state.in_flight:
121+
while state.ready:
122+
state.submit(state.ready.popleft())
123+
if not state.in_flight:
77124
break
78-
done, _ = wait(list(in_flight), return_when=FIRST_COMPLETED)
79-
for future in done:
80-
node_id = in_flight.pop(future)
81-
failed = False
82-
try:
83-
value: Any = future.result()
84-
except Exception as err: # pylint: disable=broad-except
85-
value = repr(err)
86-
failed = True
87-
with lock:
88-
results[node_id] = value
89-
for dependent in graph.get(node_id, ()):
90-
indegree[dependent] -= 1
91-
if failed and fail_fast:
92-
_mark_skipped(dependent, node_id, graph, indegree, results, lock)
93-
elif indegree[dependent] == 0 and dependent not in results:
94-
ready.append(dependent)
95-
96-
return results
125+
state.drain_completed()
126+
return state.results
97127

98128

99129
def _run_action(action: list) -> Any:
@@ -156,20 +186,3 @@ def _detect_cycle(
156186
queue.append(dependent)
157187
if visited != len(ids):
158188
raise DagException("cycle detected in DAG")
159-
160-
161-
def _mark_skipped(
162-
dependent: str,
163-
reason_id: str,
164-
graph: dict[str, list[str]],
165-
indegree: dict[str, int],
166-
results: dict[str, Any],
167-
lock: threading.Lock,
168-
) -> None:
169-
with lock:
170-
if dependent in results:
171-
return
172-
results[dependent] = f"skipped: dep {reason_id!r} failed"
173-
for grandchild in graph.get(dependent, ()):
174-
indegree[grandchild] -= 1
175-
_mark_skipped(grandchild, dependent, graph, indegree, results, lock)

automation_file/core/fim.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pathlib import Path
2424
from typing import Any
2525

26-
from automation_file.core.manifest import ManifestException, verify_manifest
26+
from automation_file.core.manifest import verify_manifest
2727
from automation_file.exceptions import FileAutomationException
2828
from automation_file.logging_config import file_automation_logger
2929
from automation_file.notify import NotificationManager, notification_manager
@@ -86,7 +86,7 @@ def check_once(self) -> dict[str, Any]:
8686
"""Run one verification pass and return the summary."""
8787
try:
8888
summary = verify_manifest(self._root, self._manifest_path)
89-
except (ManifestException, FileAutomationException) as err:
89+
except FileAutomationException as err:
9090
file_automation_logger.error("integrity_monitor: verify failed: %r", err)
9191
summary = {
9292
"matched": [],

automation_file/core/metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def record_action(action: str, duration_seconds: float, ok: bool) -> None:
5656
try:
5757
ACTION_COUNT.labels(action=action, status=status).inc()
5858
ACTION_DURATION.labels(action=action).observe(max(0.0, float(duration_seconds)))
59-
except Exception as err: # pragma: no cover - defensive
59+
except Exception as err: # pylint: disable=broad-except # pragma: no cover - defensive
6060
file_automation_logger.error("metrics.record_action failed: %r", err)
6161

6262

automation_file/core/package_loader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ def load(self, package: str) -> ModuleType | None:
3232
file_automation_logger.error("PackageLoader: cannot find %s", package)
3333
return None
3434
try:
35-
# nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import
3635
# `package` is a trusted caller-supplied name (see PackageLoader docstring and
3736
# the CLAUDE.md security note on plugin loading); it is not untrusted input.
38-
module = import_module(spec.name)
39-
except (ImportError, ModuleNotFoundError) as error:
37+
name = spec.name
38+
module = import_module(name) # nosemgrep
39+
except ImportError as error:
4040
file_automation_logger.error("PackageLoader import error: %r", error)
4141
return None
4242
self._cache[package] = module

automation_file/core/plugins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,4 @@ def _iter_entry_points() -> list[EntryPoint]:
8383
except TypeError:
8484
# importlib.metadata before 3.10 used a different API; the project
8585
# targets 3.10+, so this branch exists only as defensive padding.
86-
return list(entry_points().get(ENTRY_POINT_GROUP, []))
86+
return list(entry_points().get(ENTRY_POINT_GROUP, [])) # pylint: disable=no-member

automation_file/core/substitution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from automation_file.exceptions import FileAutomationException
2525

26-
_PATTERN = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]*))?\}")
26+
_PATTERN = re.compile(r"\$\{([a-zA-Z_]\w*)(?::([^}]*))?\}", re.ASCII)
2727

2828

2929
class SubstitutionException(FileAutomationException):

automation_file/local/dir_ops.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def copy_dir(dir_path: str, target_dir_path: str) -> bool:
1818
shutil.copytree(source, Path(target_dir_path), dirs_exist_ok=True)
1919
file_automation_logger.info("copy_dir: %s -> %s", source, target_dir_path)
2020
return True
21-
except (OSError, shutil.Error) as error:
21+
except OSError as error:
2222
file_automation_logger.error("copy_dir failed: %r", error)
2323
return False
2424

@@ -32,7 +32,7 @@ def remove_dir_tree(dir_path: str) -> bool:
3232
shutil.rmtree(path)
3333
file_automation_logger.info("remove_dir_tree: %s", path)
3434
return True
35-
except (OSError, shutil.Error) as error:
35+
except OSError as error:
3636
file_automation_logger.error("remove_dir_tree failed: %r", error)
3737
return False
3838

0 commit comments

Comments
 (0)