Skip to content

Commit d81cc26

Browse files
committed
first pass + utest
1 parent a498e7c commit d81cc26

File tree

3 files changed

+180
-3
lines changed

3 files changed

+180
-3
lines changed

nodescraper/cli/cli.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
from nodescraper.cli.constants import DEFAULT_CONFIG, META_VAR_MAP
3636
from nodescraper.cli.dynamicparserbuilder import DynamicParserBuilder
3737
from nodescraper.cli.helper import (
38+
dump_results_to_csv,
3839
generate_reference_config,
3940
generate_reference_config_from_logs,
41+
generate_summary,
4042
get_plugin_configs,
4143
get_system_info,
4244
log_system_info,
@@ -154,6 +156,18 @@ def build_parser(
154156

155157
subparsers = parser.add_subparsers(dest="subcmd", help="Subcommands")
156158

159+
summary_parser = subparsers.add_parser(
160+
"summary",
161+
help="Generates summary csv file",
162+
)
163+
164+
summary_parser.add_argument(
165+
"--summary_path",
166+
dest="summary_path",
167+
type=log_path_arg,
168+
help="Path to node-scraper results. Generates summary csv file in summary.csv.",
169+
)
170+
157171
run_plugin_parser = subparsers.add_parser(
158172
"run-plugins",
159173
help="Run a series of plugins",
@@ -327,12 +341,13 @@ def main(arg_input: Optional[list[str]] = None):
327341

328342
parsed_args = parser.parse_args(top_level_args)
329343
system_info = get_system_info(parsed_args)
344+
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
345+
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%I_%M_%S_%p")
330346

331347
if parsed_args.log_path and parsed_args.subcmd not in ["gen-plugin-config", "describe"]:
332-
sname = system_info.name.lower().replace("-", "_").replace(".", "_")
333348
log_path = os.path.join(
334349
parsed_args.log_path,
335-
f"scraper_logs_{sname}_{datetime.datetime.now().strftime('%Y_%m_%d-%I_%M_%S_%p')}",
350+
f"scraper_logs_{sname}_{timestamp}",
336351
)
337352
os.makedirs(log_path)
338353
else:
@@ -342,6 +357,10 @@ def main(arg_input: Optional[list[str]] = None):
342357
if log_path:
343358
logger.info("Log path: %s", log_path)
344359

360+
if parsed_args.subcmd == "summary":
361+
generate_summary(parsed_args.summary_path, logger)
362+
sys.exit(0)
363+
345364
if parsed_args.subcmd == "describe":
346365
parse_describe(parsed_args, plugin_reg, config_reg, logger)
347366

@@ -407,6 +426,8 @@ def main(arg_input: Optional[list[str]] = None):
407426
try:
408427
results = plugin_executor.run_queue()
409428

429+
dump_results_to_csv(results, sname, log_path, timestamp, logger)
430+
410431
if parsed_args.reference_config:
411432
ref_config = generate_reference_config(results, plugin_reg, logger)
412433
path = os.path.join(os.getcwd(), "reference_config.json")

nodescraper/cli/helper.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#
2525
###############################################################################
2626
import argparse
27+
import csv
28+
import glob
2729
import json
2830
import logging
2931
import os
@@ -422,3 +424,76 @@ def find_datamodel_and_result(base_path: str) -> list[Tuple[str, str]]:
422424
tuple_list.append((datamodel_path, result_path))
423425

424426
return tuple_list
427+
428+
429+
def dump_results_to_csv(
430+
results: list[PluginResult],
431+
nodename: str,
432+
log_path: str,
433+
timestamp: str,
434+
logger: logging.Logger,
435+
):
436+
"""dump node-scraper summary results to csv file
437+
438+
Args:
439+
results (list[PluginResult]): list of PluginResults
440+
nodename (str): node where results come from
441+
log_path (str): path to results
442+
timestamp (str): time when results were taken
443+
logger (logging.Logger): instance of logger
444+
"""
445+
fieldnames = ["nodename", "plugin", "status", "timestamp", "message"]
446+
filename = log_path + "/errorscraper.csv"
447+
all_rows = []
448+
for res in results:
449+
row = {
450+
"nodename": nodename,
451+
"plugin": res.source,
452+
"status": res.status.name,
453+
"timestamp": timestamp,
454+
"message": res.message,
455+
}
456+
all_rows.append(row)
457+
dump_to_csv(all_rows, filename, fieldnames, logger)
458+
459+
460+
def dump_to_csv(all_rows: list, filename: str, fieldnames: list[str], logger: logging.Logger):
461+
"""dump data to csv
462+
463+
Args:
464+
all_rows (list): rows to be written
465+
filename (str): name of file to write to
466+
fieldnames (list[str]): header for csv file
467+
logger (logging.Logger): isntance of logger
468+
"""
469+
try:
470+
with open(filename, "w", newline="") as f:
471+
writer = csv.DictWriter(f, fieldnames=fieldnames)
472+
writer.writeheader()
473+
for row in all_rows:
474+
writer.writerow(row)
475+
except Exception as exp:
476+
logger.error("Could not dump data to csv file: %s", exp)
477+
logger.info("Data written to csv file: %s", filename)
478+
479+
480+
def generate_summary(base_path: str, logger: logging.Logger):
481+
"""Concatenate csv files into 1 summary csv file
482+
483+
Args:
484+
base_path (str): base path to look for csv files
485+
logger (logging.Logger): instance of logger
486+
"""
487+
fieldnames = ["nodename", "plugin", "status", "timestamp", "message"]
488+
all_rows = []
489+
490+
pattern = os.path.join(base_path, "**", "errorscraper.csv")
491+
for filepath in glob.glob(pattern, recursive=True):
492+
logger.info(f"Reading: {filepath}")
493+
with open(filepath, newline="") as f:
494+
reader = csv.DictReader(f)
495+
for row in reader:
496+
all_rows.append(row)
497+
498+
output_path = os.path.join(base_path, "summary.csv")
499+
dump_to_csv(all_rows, output_path, fieldnames, logger)

test/unit/framework/test_cli_helper.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#
2525
###############################################################################
2626
import argparse
27+
import csv
2728
import json
2829
import logging
2930
import os
@@ -35,7 +36,13 @@
3536
from pydantic import BaseModel
3637

3738
from nodescraper.cli import cli
38-
from nodescraper.cli.helper import build_config, find_datamodel_and_result
39+
from nodescraper.cli.helper import (
40+
build_config,
41+
dump_results_to_csv,
42+
dump_to_csv,
43+
find_datamodel_and_result,
44+
generate_summary,
45+
)
3946
from nodescraper.configregistry import ConfigRegistry
4047
from nodescraper.enums import ExecutionStatus, SystemInteractionLevel
4148
from nodescraper.models import PluginConfig, TaskResult
@@ -176,3 +183,77 @@ def build_from_model(cls, datamodel):
176183
assert isinstance(cfg, PluginConfig)
177184
assert set(cfg.plugins) == {parent}
178185
assert cfg.plugins[parent]["analysis_args"] == {}
186+
187+
188+
def test_dump_to_csv(tmp_path):
189+
logger = logging.getLogger()
190+
data = [
191+
{
192+
"nodename": "node1",
193+
"plugin": "TestPlugin",
194+
"status": "OK",
195+
"timestamp": "2025_07_16-12_00_00_PM",
196+
"message": "Success",
197+
}
198+
]
199+
filename = tmp_path / "test.csv"
200+
fieldnames = list(data[0].keys())
201+
202+
dump_to_csv(data, str(filename), fieldnames, logger)
203+
204+
with open(filename, newline="") as f:
205+
reader = list(csv.DictReader(f))
206+
assert reader == data
207+
208+
209+
def test_dump_results_to_csv(tmp_path, caplog):
210+
logger = logging.getLogger()
211+
212+
result = PluginResult(
213+
source="TestPlugin", status=ExecutionStatus.OK, message="some message", result_data={}
214+
)
215+
216+
dump_results_to_csv([result], "node123", str(tmp_path), "2025_07_16-01_00_00_PM", logger)
217+
218+
out_file = tmp_path / "errorscraper.csv"
219+
assert out_file.exists()
220+
221+
with open(out_file, newline="") as f:
222+
reader = list(csv.DictReader(f))
223+
assert reader[0]["nodename"] == "node123"
224+
assert reader[0]["plugin"] == "TestPlugin"
225+
assert reader[0]["status"] == "OK"
226+
assert reader[0]["message"] == "some message"
227+
228+
229+
def test_generate_summary(tmp_path):
230+
logger = logging.getLogger()
231+
232+
subdir = tmp_path / "sub"
233+
subdir.mkdir()
234+
235+
errorscraper_path = subdir / "errorscraper.csv"
236+
with open(errorscraper_path, "w", newline="") as f:
237+
writer = csv.DictWriter(
238+
f, fieldnames=["nodename", "plugin", "status", "timestamp", "message"]
239+
)
240+
writer.writeheader()
241+
writer.writerow(
242+
{
243+
"nodename": "nodeX",
244+
"plugin": "PluginA",
245+
"status": "OK",
246+
"timestamp": "2025_07_16-01_00_00_PM",
247+
"message": "some message",
248+
}
249+
)
250+
251+
generate_summary(str(tmp_path), logger)
252+
253+
summary_path = tmp_path / "summary.csv"
254+
assert summary_path.exists()
255+
256+
with open(summary_path, newline="") as f:
257+
rows = list(csv.DictReader(f))
258+
assert len(rows) == 1
259+
assert rows[0]["plugin"] == "PluginA"

0 commit comments

Comments
 (0)