Skip to content

Commit e1fee80

Browse files
Gerrrrhenrikingomfleming
authored
Add JsonImporter (#22)
Co-authored-by: Henrik Ingo <[email protected]> Co-authored-by: Matt Fleming <[email protected]>
1 parent 84e3e74 commit e1fee80

File tree

2 files changed

+156
-6
lines changed

2 files changed

+156
-6
lines changed

hunter/importer.py

+125-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import csv
2+
import json
23
from collections import OrderedDict
34
from contextlib import contextmanager
45
from dataclasses import dataclass
@@ -16,6 +17,7 @@
1617
CsvTestConfig,
1718
GraphiteTestConfig,
1819
HistoStatTestConfig,
20+
JsonTestConfig,
1921
PostgresMetric,
2022
PostgresTestConfig,
2123
TestConfig,
@@ -268,7 +270,7 @@ def fetch_data(self, test_conf: TestConfig, selector: DataSelector = DataSelecto
268270
# Read metric values. Note we can still fail on conversion to float,
269271
# because the user is free to override the column selection and thus
270272
# they may select a column that contains non-numeric data:
271-
for (name, i) in zip(metric_names, metric_indexes):
273+
for name, i in zip(metric_names, metric_indexes):
272274
try:
273275
data[name].append(float(row[i]))
274276
except ValueError as err:
@@ -498,7 +500,7 @@ def fetch_data(self, test_conf: TestConfig, selector: DataSelector = DataSelecto
498500
# Read metric values. Note we can still fail on conversion to float,
499501
# because the user is free to override the column selection and thus
500502
# they may select a column that contains non-numeric data:
501-
for (name, i) in zip(metric_names, metric_indexes):
503+
for name, i in zip(metric_names, metric_indexes):
502504
try:
503505
data[name].append(float(row[i]))
504506
except ValueError as err:
@@ -537,19 +539,133 @@ def fetch_all_metric_names(self, test_conf: PostgresTestConfig) -> List[str]:
537539
return [m for m in test_conf.metrics.keys()]
538540

539541

542+
class JsonImporter(Importer):
543+
def __init__(self):
544+
self._data = {}
545+
546+
@staticmethod
547+
def _read_json_file(filename: str):
548+
try:
549+
return json.load(open(filename))
550+
except FileNotFoundError:
551+
raise DataImportError(f"Input file not found: {filename}")
552+
553+
def inputfile(self, test_conf: JsonTestConfig):
554+
if test_conf.file not in self._data:
555+
self._data[test_conf.file] = self._read_json_file(test_conf.file)
556+
return self._data[test_conf.file]
557+
558+
def fetch_data(self, test_conf: TestConfig, selector: DataSelector = DataSelector()) -> Series:
559+
560+
if not isinstance(test_conf, JsonTestConfig):
561+
raise ValueError("Expected JsonTestConfig")
562+
563+
# TODO: refactor. THis is copy pasted from CSV importer
564+
since_time = selector.since_time
565+
until_time = selector.until_time
566+
567+
if since_time.timestamp() > until_time.timestamp():
568+
raise DataImportError(
569+
f"Invalid time range: ["
570+
f"{format_timestamp(int(since_time.timestamp()))}, "
571+
f"{format_timestamp(int(until_time.timestamp()))}]"
572+
)
573+
574+
time = []
575+
data = OrderedDict()
576+
metrics = OrderedDict()
577+
attributes = OrderedDict()
578+
579+
for name in self.fetch_all_metric_names(test_conf):
580+
# Ignore metrics if selector.metrics is not None and name is not in selector.metrics
581+
if selector.metrics is not None and name not in selector.metrics:
582+
continue
583+
data[name] = []
584+
585+
attr_names = self.fetch_all_attribute_names(test_conf)
586+
for name in attr_names:
587+
attributes[name] = []
588+
589+
# If the user specified a branch, only include results from that branch.
590+
# Otherwise if the test config specifies a branch, only include results from that branch.
591+
# Else include all results.
592+
branch = None
593+
if selector.branch:
594+
branch = selector.branch
595+
elif test_conf.base_branch:
596+
branch = test_conf.base_branch
597+
598+
objs = self.inputfile(test_conf)
599+
list_of_json_obj = []
600+
for o in objs:
601+
if branch and o["attributes"]["branch"] != branch:
602+
continue
603+
list_of_json_obj.append(o)
604+
605+
for result in list_of_json_obj:
606+
time.append(result["timestamp"])
607+
for metric in result["metrics"]:
608+
# Skip metrics not in selector.metrics if selector.metrics is enabled
609+
if metric["name"] not in data:
610+
continue
611+
612+
data[metric["name"]].append(metric["value"])
613+
metrics[metric["name"]] = Metric(1, 1.0)
614+
for a in attr_names:
615+
attributes[a] = [o["attributes"][a] for o in list_of_json_obj]
616+
617+
# Leave last n points:
618+
time = time[-selector.last_n_points :]
619+
tmp = data
620+
data = {}
621+
for k, v in tmp.items():
622+
data[k] = v[-selector.last_n_points :]
623+
tmp = attributes
624+
attributes = {}
625+
for k, v in tmp.items():
626+
attributes[k] = v[-selector.last_n_points :]
627+
628+
return Series(
629+
test_conf.name,
630+
branch=None,
631+
time=time,
632+
metrics=metrics,
633+
data=data,
634+
attributes=attributes,
635+
)
636+
637+
def fetch_all_metric_names(self, test_conf: JsonTestConfig) -> List[str]:
638+
metric_names = set()
639+
list_of_json_obj = self.inputfile(test_conf)
640+
for result in list_of_json_obj:
641+
for metric in result["metrics"]:
642+
metric_names.add(metric["name"])
643+
return [m for m in metric_names]
644+
645+
def fetch_all_attribute_names(self, test_conf: JsonTestConfig) -> List[str]:
646+
attr_names = set()
647+
list_of_json_obj = self.inputfile(test_conf)
648+
for result in list_of_json_obj:
649+
for a in result["attributes"].keys():
650+
attr_names.add(a)
651+
return [m for m in attr_names]
652+
653+
540654
class Importers:
541655
__config: Config
542656
__csv_importer: Optional[CsvImporter]
543657
__graphite_importer: Optional[GraphiteImporter]
544658
__histostat_importer: Optional[HistoStatImporter]
545659
__postgres_importer: Optional[PostgresImporter]
660+
__json_importer: Optional[JsonImporter]
546661

547662
def __init__(self, config: Config):
548663
self.__config = config
549664
self.__csv_importer = None
550665
self.__graphite_importer = None
551666
self.__histostat_importer = None
552667
self.__postgres_importer = None
668+
self.__json_importer = None
553669

554670
def csv_importer(self) -> CsvImporter:
555671
if self.__csv_importer is None:
@@ -571,6 +687,11 @@ def postgres_importer(self) -> PostgresImporter:
571687
self.__postgres_importer = PostgresImporter(Postgres(self.__config.postgres))
572688
return self.__postgres_importer
573689

690+
def json_importer(self) -> JsonImporter:
691+
if self.__json_importer is None:
692+
self.__json_importer = JsonImporter()
693+
return self.__json_importer
694+
574695
def get(self, test: TestConfig) -> Importer:
575696
if isinstance(test, CsvTestConfig):
576697
return self.csv_importer()
@@ -580,5 +701,7 @@ def get(self, test: TestConfig) -> Importer:
580701
return self.histostat_importer()
581702
elif isinstance(test, PostgresTestConfig):
582703
return self.postgres_importer()
704+
elif isinstance(test, JsonTestConfig):
705+
return self.json_importer()
583706
else:
584707
raise ValueError(f"Unsupported test type {type(test)}")

hunter/test_config.py

+31-4
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def create_test_config(name: str, config: Dict) -> TestConfig:
162162
Loads properties of a test from a dictionary read from hunter's config file
163163
This dictionary must have the `type` property to determine the type of the test.
164164
Other properties depend on the type.
165-
Currently supported test types are `fallout`, `graphite`, `csv`, and `psql`.
165+
Currently supported test types are `fallout`, `graphite`, `csv`, `json`, and `psql`.
166166
"""
167167
test_type = config.get("type")
168168
if test_type == "csv":
@@ -173,6 +173,8 @@ def create_test_config(name: str, config: Dict) -> TestConfig:
173173
return create_histostat_test_config(name, config)
174174
elif test_type == "postgres":
175175
return create_postgres_test_config(name, config)
176+
elif test_type == "json":
177+
return create_json_test_config(name, config)
176178
elif test_type is None:
177179
raise TestConfigError(f"Test type not set for test {name}")
178180
else:
@@ -192,7 +194,7 @@ def create_csv_test_config(test_name: str, test_info: Dict) -> CsvTestConfig:
192194
for name in metrics_info:
193195
metrics.append(CsvMetric(name, 1, 1.0, name))
194196
elif isinstance(metrics_info, Dict):
195-
for (metric_name, metric_conf) in metrics_info.items():
197+
for metric_name, metric_conf in metrics_info.items():
196198
metrics.append(
197199
CsvMetric(
198200
name=metric_name,
@@ -231,7 +233,7 @@ def create_graphite_test_config(name: str, test_info: Dict) -> GraphiteTestConfi
231233

232234
metrics = []
233235
try:
234-
for (metric_name, metric_conf) in metrics_info.items():
236+
for metric_name, metric_conf in metrics_info.items():
235237
metrics.append(
236238
GraphiteMetric(
237239
name=metric_name,
@@ -279,7 +281,7 @@ def create_postgres_test_config(test_name: str, test_info: Dict) -> PostgresTest
279281
for name in metrics_info:
280282
metrics.append(CsvMetric(name, 1, 1.0))
281283
elif isinstance(metrics_info, Dict):
282-
for (metric_name, metric_conf) in metrics_info.items():
284+
for metric_name, metric_conf in metrics_info.items():
283285
metrics.append(
284286
PostgresMetric(
285287
name=metric_name,
@@ -294,3 +296,28 @@ def create_postgres_test_config(test_name: str, test_info: Dict) -> PostgresTest
294296
return PostgresTestConfig(test_name, query, update_stmt, time_column, metrics, attributes)
295297
except KeyError as e:
296298
raise TestConfigError(f"Configuration key not found in test {test_name}: {e.args[0]}")
299+
300+
301+
@dataclass
302+
class JsonTestConfig(TestConfig):
303+
name: str
304+
file: str
305+
base_branch: str
306+
307+
# TODO: This should return the list defined in the config file hunter.yaml
308+
def fully_qualified_metric_names(self):
309+
from hunter.importer import JsonImporter
310+
311+
metric_names = JsonImporter().fetch_all_metric_names(self)
312+
return [f"{self.name}.{m}" for m in metric_names]
313+
314+
315+
def create_json_test_config(name: str, test_info: Dict) -> JsonTestConfig:
316+
try:
317+
file = test_info["file"]
318+
except KeyError as e:
319+
raise TestConfigError(f"Configuration key not found in test {name}: {e.args[0]}")
320+
if not os.path.exists(file):
321+
raise TestConfigError(f"Configuration file not found: {file}")
322+
base_branch = test_info.get("base_branch", None)
323+
return JsonTestConfig(name, file, base_branch)

0 commit comments

Comments
 (0)