argus-alm 0.12.7__tar.gz → 0.12.8__tar.gz

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 (90) hide show
  1. {argus_alm-0.12.7 → argus_alm-0.12.8}/PKG-INFO +1 -1
  2. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/api.py +7 -4
  3. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/client_api.py +1 -5
  4. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/testrun_api.py +2 -1
  5. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/models/result.py +18 -6
  6. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/testrun.py +7 -3
  7. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/client_service.py +9 -3
  8. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/results_service.py +73 -12
  9. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/testrun.py +0 -22
  10. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/generic_result.py +10 -5
  11. argus_alm-0.12.8/argus/client/generic_result_old.py +143 -0
  12. {argus_alm-0.12.7 → argus_alm-0.12.8}/pyproject.toml +2 -2
  13. {argus_alm-0.12.7 → argus_alm-0.12.8}/LICENSE +0 -0
  14. {argus_alm-0.12.7 → argus_alm-0.12.8}/README.md +0 -0
  15. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/__init__.py +0 -0
  16. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/.gitkeep +0 -0
  17. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/__init__.py +0 -0
  18. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/cli.py +0 -0
  19. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/__init__.py +0 -0
  20. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/admin.py +0 -0
  21. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/admin_api.py +0 -0
  22. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/auth.py +0 -0
  23. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/main.py +0 -0
  24. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/notification_api.py +0 -0
  25. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/notifications.py +0 -0
  26. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/team.py +0 -0
  27. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/team_ui.py +0 -0
  28. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/controller/view_api.py +0 -0
  29. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/db.py +0 -0
  30. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/error_handlers.py +0 -0
  31. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/events/event_processors.py +0 -0
  32. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/models/__init__.py +0 -0
  33. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/models/web.py +0 -0
  34. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/__init__.py +0 -0
  35. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/core.py +0 -0
  36. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/driver_matrix_tests/controller.py +0 -0
  37. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/driver_matrix_tests/model.py +0 -0
  38. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/driver_matrix_tests/plugin.py +0 -0
  39. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/driver_matrix_tests/raw_types.py +0 -0
  40. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/driver_matrix_tests/service.py +0 -0
  41. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/driver_matrix_tests/udt.py +0 -0
  42. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/generic/model.py +0 -0
  43. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/generic/plugin.py +0 -0
  44. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/generic/types.py +0 -0
  45. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/loader.py +0 -0
  46. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/controller.py +0 -0
  47. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/plugin.py +0 -0
  48. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/resource_setup.py +0 -0
  49. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/service.py +0 -0
  50. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/types.py +0 -0
  51. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sct/udt.py +0 -0
  52. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sirenada/model.py +0 -0
  53. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sirenada/plugin.py +0 -0
  54. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/plugins/sirenada/types.py +0 -0
  55. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/admin.py +0 -0
  56. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/argus_service.py +0 -0
  57. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/build_system_monitor.py +0 -0
  58. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/event_service.py +0 -0
  59. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/jenkins_service.py +0 -0
  60. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/notification_manager.py +0 -0
  61. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/release_manager.py +0 -0
  62. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/stats.py +0 -0
  63. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/team_manager_service.py +0 -0
  64. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/user.py +0 -0
  65. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/service/views.py +0 -0
  66. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/template_filters.py +0 -0
  67. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/common.py +0 -0
  68. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/config.py +0 -0
  69. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/encoders.py +0 -0
  70. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/enums.py +0 -0
  71. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/logsetup.py +0 -0
  72. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/module_loaders.py +0 -0
  73. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/backend/util/send_email.py +0 -0
  74. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/__init__.py +0 -0
  75. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/base.py +0 -0
  76. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/driver_matrix_tests/cli.py +0 -0
  77. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/driver_matrix_tests/client.py +0 -0
  78. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/generic/cli.py +0 -0
  79. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/generic/client.py +0 -0
  80. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/sct/client.py +0 -0
  81. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/sct/types.py +0 -0
  82. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/client/sirenada/client.py +0 -0
  83. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/.gitkeep +0 -0
  84. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/argus_json.py +0 -0
  85. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/cloud_types.py +0 -0
  86. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/config.py +0 -0
  87. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/db_types.py +0 -0
  88. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/interface.py +0 -0
  89. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/testrun.py +0 -0
  90. {argus_alm-0.12.7 → argus_alm-0.12.8}/argus/db/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: argus-alm
3
- Version: 0.12.7
3
+ Version: 0.12.8
4
4
  Summary: Argus
5
5
  Home-page: https://github.com/scylladb/argus
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ import requests
4
4
  from flask import (
5
5
  Blueprint,
6
6
  g,
7
- request
7
+ request, Response
8
8
  )
9
9
  from flask.json import jsonify
10
10
  from argus.backend.error_handlers import handle_api_exception
@@ -382,18 +382,21 @@ def test_info():
382
382
  "response": info
383
383
  }
384
384
 
385
- @bp.route("/test-results", methods=["GET"])
385
+ @bp.route("/test-results", methods=["GET", "HEAD"])
386
386
  @api_login_required
387
387
  def test_results():
388
388
  test_id = request.args.get("testId")
389
389
  if not test_id:
390
390
  raise Exception("No testId provided")
391
391
  service = ResultsService()
392
- info = service.get_results(test_id=UUID(test_id))
392
+ if request.method == 'HEAD':
393
+ exists = service.is_results_exist(test_id=UUID(test_id))
394
+ return Response(status=200 if exists else 404)
395
+ graphs, ticks = service.get_test_graphs(test_id=UUID(test_id))
393
396
 
394
397
  return {
395
398
  "status": "ok",
396
- "response": info
399
+ "response": {"graphs": graphs, "ticks": ticks}
397
400
  }
398
401
 
399
402
  @bp.route("/test_run/comment/get", methods=["GET"]) # TODO: remove
@@ -105,8 +105,4 @@ def run_finalize(run_type: str, run_id: str):
105
105
  @api_login_required
106
106
  def submit_results(run_type: str, run_id: str):
107
107
  payload = get_payload(request)
108
- result = ClientService().submit_results(run_type=run_type, run_id=run_id, results=payload)
109
- return {
110
- "status": "ok",
111
- "response": result
112
- }
108
+ return ClientService().submit_results(run_type=run_type, run_id=run_id, results=payload)
@@ -8,6 +8,7 @@ from flask import (
8
8
  from argus.backend.error_handlers import handle_api_exception
9
9
  from argus.backend.models.web import ArgusTest
10
10
  from argus.backend.service.jenkins_service import JenkinsService
11
+ from argus.backend.service.results_service import ResultsService
11
12
  from argus.backend.service.testrun import TestRunService
12
13
  from argus.backend.service.user import api_login_required
13
14
  from argus.backend.util.common import get_payload
@@ -67,7 +68,7 @@ def test_run_activity(run_id: str):
67
68
  @bp.route("/run/<string:test_id>/<string:run_id>/fetch_results", methods=["GET"])
68
69
  @api_login_required
69
70
  def fetch_results(test_id: str, run_id: str):
70
- tables = TestRunService().fetch_results(test_id=UUID(test_id), run_id=UUID(run_id))
71
+ tables = ResultsService().get_run_results(test_id=UUID(test_id), run_id=UUID(run_id))
71
72
  return {
72
73
  "status": "ok",
73
74
  "tables": tables
@@ -1,14 +1,17 @@
1
1
  from cassandra.cqlengine import columns
2
2
  from cassandra.cqlengine.models import Model
3
3
  from cassandra.cqlengine.usertype import UserType
4
- from enum import Enum
5
4
 
5
+ class BestResult(UserType):
6
+ date = columns.DateTime()
7
+ value = columns.Double()
8
+ run_id = columns.UUID()
6
9
 
7
- class Status(Enum):
8
- PASS = 0
9
- WARNING = 1
10
- ERROR = 2
11
-
10
+ class ValidationRules(UserType):
11
+ higher_is_better = columns.Boolean()
12
+ margin_percent = columns.Double()
13
+ margin_value = columns.Double()
14
+ limit = columns.Double()
12
15
 
13
16
  class ColumnMetadata(UserType):
14
17
  name = columns.Ascii()
@@ -22,10 +25,14 @@ class ArgusGenericResultMetadata(Model):
22
25
  name = columns.Text(required=True, primary_key=True)
23
26
  description = columns.Text()
24
27
  columns_meta = columns.List(value_type=columns.UserDefinedType(ColumnMetadata))
28
+ validation_rules = columns.Map(key_type=columns.Ascii(), value_type=columns.List(value_type=columns.UserDefinedType(ValidationRules)))
29
+ best_results = columns.Map(key_type=columns.Ascii(), value_type=columns.UserDefinedType(BestResult))
25
30
  rows_meta = columns.List(value_type=columns.Ascii())
26
31
 
27
32
  def __init__(self, **kwargs):
28
33
  kwargs["columns_meta"] = [ColumnMetadata(**col) for col in kwargs.pop('columns_meta', [])]
34
+ kwargs["best_results"] = {k: [BestResult(**z) for z in v] for k, v in kwargs.pop('best_results', {}).items()}
35
+ kwargs["validation_rules"] = {k: ValidationRules(**v) for k, v in kwargs.pop('validation_rules', {}).items()}
29
36
  super().__init__(**kwargs)
30
37
 
31
38
  def update_if_changed(self, new_data: dict) -> None:
@@ -44,6 +51,10 @@ class ArgusGenericResultMetadata(Model):
44
51
  if row not in self.rows_meta:
45
52
  added_rows.append(row)
46
53
  value = self.rows_meta + added_rows
54
+ elif field == "best_results":
55
+ value = {k: [BestResult(**z) for z in v] for k, v in value.items()}
56
+ elif field == "validation_rules":
57
+ value = {k: ValidationRules(**v) for k, v in value.items()}
47
58
  if getattr(self, field) != value:
48
59
  setattr(self, field, value)
49
60
  updated = True
@@ -60,4 +71,5 @@ class ArgusGenericResultData(Model):
60
71
  row = columns.Ascii(primary_key=True, index=True)
61
72
  sut_timestamp = columns.DateTime() # for sorting
62
73
  value = columns.Double()
74
+ value_text = columns.Text()
63
75
  status = columns.Ascii()
@@ -1,6 +1,6 @@
1
1
  from enum import Enum
2
2
  import logging
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
  from dataclasses import dataclass, field
5
5
  from typing import Optional
6
6
  from uuid import UUID
@@ -254,8 +254,12 @@ class SCTTestRun(PluginModelBase):
254
254
  def sut_timestamp(self) -> float:
255
255
  """converts scylla-server date to timestamp and adds revision in subseconds precision to diffirentiate
256
256
  scylla versions from the same day. It's not perfect, but we don't know exact version time."""
257
- scylla_package = [package for package in self.packages if package.name == "scylla-server"][0]
258
- return (datetime.strptime(scylla_package.date, '%Y%m%d').timestamp()
257
+ try:
258
+ scylla_package_upgraded = [package for package in self.packages if package.name == "scylla-server-upgraded"][0]
259
+ except IndexError:
260
+ scylla_package_upgraded = None
261
+ scylla_package = scylla_package_upgraded or [package for package in self.packages if package.name == "scylla-server"][0]
262
+ return (datetime.strptime(scylla_package.date, '%Y%m%d').replace(tzinfo=timezone.utc).timestamp()
259
263
  + int(scylla_package.revision_id, 16) % 1000000 / 1000000)
260
264
 
261
265
 
@@ -79,9 +79,15 @@ class ClientService:
79
79
 
80
80
  return "Finalized"
81
81
 
82
- def submit_results(self, run_type: str, run_id: str, results: dict) -> str:
82
+ def submit_results(self, run_type: str, run_id: str, results: dict) -> dict[str, str]:
83
83
  model = self.get_model(run_type)
84
- run = model.load_test_run(UUID(run_id))
84
+ try:
85
+ run = model.load_test_run(UUID(run_id))
86
+ except model.DoesNotExist:
87
+ return {"status": "error", "response": {
88
+ "exception": "DoesNotExist",
89
+ "arguments": [run_id]
90
+ }}
85
91
  existing_table = ArgusGenericResultMetadata.objects(test_id=run.test_id, name=results["meta"]["name"]).first()
86
92
  if existing_table:
87
93
  existing_table.update_if_changed(results["meta"])
@@ -98,4 +104,4 @@ class ClientService:
98
104
  sut_timestamp=sut_timestamp,
99
105
  **cell
100
106
  ).save()
101
- return "Submitted"
107
+ return {"status": "ok", "message": "Results submitted"}
@@ -67,12 +67,12 @@ colors = [
67
67
  ]
68
68
 
69
69
 
70
- def get_sorted_data_for_column_and_row(data: List[Dict[str, Any]], column: str, row: str) -> List[Dict[str, Any]]:
70
+ def get_sorted_data_for_column_and_row(data: List[ArgusGenericResultData], column: str, row: str) -> List[Dict[str, Any]]:
71
71
  return sorted([{"x": entry.sut_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'),
72
72
  "y": entry.value,
73
73
  "id": entry.run_id}
74
- for entry in data if entry['column'] == column and entry['row'] == row],
75
- key=lambda x: x['x'])
74
+ for entry in data if entry.column == column and entry.row == row],
75
+ key=lambda point: point["x"])
76
76
 
77
77
 
78
78
  def get_min_max_y(datasets: List[Dict[str, Any]]) -> (float, float):
@@ -105,6 +105,9 @@ def round_datasets_to_min_max(datasets: List[Dict[str, Any]], min_y: float, max_
105
105
  def create_chartjs(table, data):
106
106
  graphs = []
107
107
  for column in table.columns_meta:
108
+ if column.type == "TEXT":
109
+ # skip text columns
110
+ continue
108
111
  datasets = [
109
112
  {"label": row,
110
113
  "borderColor": colors[idx % len(colors)],
@@ -118,7 +121,7 @@ def create_chartjs(table, data):
118
121
  continue
119
122
  options = copy.deepcopy(default_options)
120
123
  options["plugins"]["title"]["text"] = f"{table.name} - {column.name}"
121
- options["scales"]["y"]["title"]["text"] = f"[{column.unit}]"
124
+ options["scales"]["y"]["title"]["text"] = f"[{column.unit}]" if column.unit else ""
122
125
  options["scales"]["y"]["min"] = min_y
123
126
  options["scales"]["y"]["max"] = max_y
124
127
  graphs.append({"options": options, "data":
@@ -126,15 +129,73 @@ def create_chartjs(table, data):
126
129
  return graphs
127
130
 
128
131
 
129
- class ResultsService:
132
+ def calculate_graph_ticks(graphs: List[Dict]) -> dict[str, str]:
133
+ min_x, max_x = None, None
134
+
135
+ for graph in graphs:
136
+ for dataset in graph["data"]["datasets"]:
137
+ if not dataset["data"]:
138
+ continue
139
+ first_x = dataset["data"][0]["x"]
140
+ last_x = dataset["data"][-1]["x"]
141
+ if min_x is None or first_x < min_x:
142
+ min_x = first_x
143
+ if max_x is None or last_x > max_x:
144
+ max_x = last_x
145
+ return {"min": min_x[:10], "max": max_x[:10]}
130
146
 
131
- def __init__(self, database_session=None):
132
- self.session = database_session if database_session else ScyllaCluster.get_session()
133
147
 
134
- def get_results(self, test_id: UUID):
148
+ class ResultsService:
149
+
150
+ def __init__(self):
151
+ self.cluster = ScyllaCluster.get()
152
+
153
+ def _get_tables_metadata(self, test_id: UUID) -> list[ArgusGenericResultMetadata]:
154
+ query_fields = ["name", "description", "columns_meta", "rows_meta"]
155
+ raw_query = (f"SELECT {','.join(query_fields)}"
156
+ f" FROM generic_result_metadata_v1 WHERE test_id = ?")
157
+ query = self.cluster.prepare(raw_query)
158
+ tables_meta = self.cluster.session.execute(query=query, parameters=(test_id,))
159
+ return [ArgusGenericResultMetadata(**table) for table in tables_meta]
160
+
161
+ def get_run_results(self, test_id: UUID, run_id: UUID) -> list[dict]:
162
+ query_fields = ["column", "row", "value", "value_text", "status"]
163
+ raw_query = (f"SELECT {','.join(query_fields)},WRITETIME(status) as ordering"
164
+ f" FROM generic_result_data_v1 WHERE test_id = ? AND run_id = ? AND name = ?")
165
+ query = self.cluster.prepare(raw_query)
166
+ tables_meta = self._get_tables_metadata(test_id=test_id)
167
+ tables = []
168
+ for table in tables_meta:
169
+ cells = self.cluster.session.execute(query=query, parameters=(test_id, run_id, table.name))
170
+ if not cells:
171
+ continue
172
+ cells = [dict(cell.items()) for cell in cells]
173
+ tables.append({'meta': {
174
+ 'name': table.name,
175
+ 'description': table.description,
176
+ 'columns_meta': table.columns_meta,
177
+ 'rows_meta': table.rows_meta,
178
+ },
179
+ 'cells': [{k: v for k, v in cell.items() if k in query_fields} for cell in cells],
180
+ 'order': min([cell['ordering'] for cell in cells] or [0])})
181
+ return sorted(tables, key=lambda x: x['order'])
182
+
183
+ def get_test_graphs(self, test_id: UUID):
184
+ query_fields = ["run_id", "column", "row", "value", "status", "sut_timestamp"]
185
+ raw_query = (f"SELECT {','.join(query_fields)}"
186
+ f" FROM generic_result_data_v1 WHERE test_id = ? AND name = ? LIMIT 2147483647")
187
+ query = self.cluster.prepare(raw_query)
188
+ tables_meta = self._get_tables_metadata(test_id=test_id)
135
189
  graphs = []
136
- res = ArgusGenericResultMetadata.objects(test_id=test_id).all()
137
- for table in res:
138
- data = ArgusGenericResultData.objects(test_id=test_id, name=table.name).all()
190
+ for table in tables_meta:
191
+ data = self.cluster.session.execute(query=query, parameters=(test_id, table.name))
192
+ data = [ArgusGenericResultData(**cell) for cell in data]
193
+ if not data:
194
+ continue
139
195
  graphs.extend(create_chartjs(table, data))
140
- return graphs
196
+ ticks = calculate_graph_ticks(graphs)
197
+ return graphs, ticks
198
+
199
+ def is_results_exist(self, test_id: UUID):
200
+ """Verify if results for given test id exist at all."""
201
+ return bool(ArgusGenericResultMetadata.objects(test_id=test_id).only(["name"]).limit(1))
@@ -307,28 +307,6 @@ class TestRunService:
307
307
  }
308
308
  return response
309
309
 
310
- def fetch_results(self, test_id: UUID, run_id: UUID) -> list[dict]:
311
- cluster = ScyllaCluster.get()
312
- query_fields = ["column", "row", "value", "status"]
313
- raw_query = (f"SELECT {','.join(query_fields)},WRITETIME(value) as ordering "
314
- f"FROM generic_result_data_v1 WHERE test_id = ? AND run_id = ? AND name = ?")
315
- query = cluster.prepare(raw_query)
316
- tables_meta = ArgusGenericResultMetadata.filter(test_id=test_id)
317
- tables = []
318
- for table in tables_meta:
319
- cells = cluster.session.execute(query=query, parameters=(test_id, run_id, table.name))
320
- if not cells:
321
- continue
322
- cells = [dict(cell.items()) for cell in cells]
323
- tables.append({'meta': {
324
- 'name': table.name,
325
- 'description': table.description,
326
- 'columns_meta': table.columns_meta,
327
- 'rows_meta': table.rows_meta,
328
- },
329
- 'cells': [{k: v for k, v in cell.items() if k in query_fields} for cell in cells],
330
- 'order': min([cell['ordering'] for cell in cells] or [0])})
331
- return sorted(tables, key=lambda x: x['order'])
332
310
 
333
311
  def submit_github_issue(self, issue_url: str, test_id: UUID, run_id: UUID):
334
312
  user_tokens = UserOauthToken.filter(user_id=g.user.id).all()
@@ -7,6 +7,7 @@ class Status(Enum):
7
7
  PASS = auto()
8
8
  WARNING = auto()
9
9
  ERROR = auto()
10
+ UNSET = auto()
10
11
 
11
12
  def __str__(self):
12
13
  return self.name
@@ -16,6 +17,7 @@ class ResultType(Enum):
16
17
  INTEGER = auto()
17
18
  FLOAT = auto()
18
19
  DURATION = auto()
20
+ TEXT = auto()
19
21
 
20
22
  def __str__(self):
21
23
  return self.name
@@ -44,7 +46,7 @@ class ResultTableMeta(type):
44
46
  cls_instance.name = meta.name
45
47
  cls_instance.description = meta.description
46
48
  cls_instance.columns = meta.Columns
47
- cls_instance.column_names = {column.name for column in cls_instance.columns}
49
+ cls_instance.column_types = {column.name: column.type for column in cls_instance.columns}
48
50
  cls_instance.rows = []
49
51
  return cls_instance
50
52
 
@@ -57,12 +59,13 @@ class Cell:
57
59
  status: Status
58
60
 
59
61
  def as_dict(self) -> dict:
60
- return {
62
+ cell = {"value_text": self.value} if isinstance(self.value, str) else {"value": self.value}
63
+ cell.update({
61
64
  "column": self.column,
62
65
  "row": self.row,
63
- "value": self.value,
64
66
  "status": str(self.status)
65
- }
67
+ })
68
+ return cell
66
69
 
67
70
 
68
71
  @dataclass
@@ -94,6 +97,8 @@ class GenericResultTable(metaclass=ResultTableMeta):
94
97
  }
95
98
 
96
99
  def add_result(self, column: str, row: str, value: Union[int, float, str], status: Status):
97
- if column not in self.column_names:
100
+ if column not in self.column_types:
98
101
  raise ValueError(f"Column {column} not found in the table")
102
+ if isinstance(value, str) and self.column_types[column] != ResultType.TEXT:
103
+ raise ValueError(f"Column {column} is not of type TEXT")
99
104
  self.results.append(Cell(column=column, row=row, value=value, status=status))
@@ -0,0 +1,143 @@
1
+ from dataclasses import dataclass, field, asdict
2
+ from typing import Dict, List, Tuple, Union, Type
3
+ from uuid import UUID
4
+ from enum import Enum
5
+ import json
6
+
7
+
8
+
9
+ class Status(Enum):
10
+ PASS = 1
11
+ WARNING = 2
12
+ ERROR = 3
13
+
14
+
15
+ class ResultType(Enum):
16
+ INTEGER = int
17
+ FLOAT = float
18
+ TEXT = str
19
+
20
+
21
+ @dataclass
22
+ class ColumnMetadata:
23
+ id: int
24
+ unit: str
25
+ type: ResultType
26
+
27
+
28
+ @dataclass
29
+ class Result:
30
+ value: Union[int, float, str]
31
+ status: Status
32
+
33
+
34
+ class ResultTableMeta(type):
35
+ def __new__(cls, name, bases, dct):
36
+ cls_instance = super().__new__(cls, name, bases, dct)
37
+ meta = dct.get('Meta')
38
+
39
+ if meta:
40
+ cls_instance.table_name = meta.table_name
41
+ cls_instance.columns_map = {col_name: ColumnMetadata(id=col_id, unit=unit, type=result_type)
42
+ for col_name, (col_id, unit, result_type) in meta.Columns.items()}
43
+ cls_instance.rows_map = {row_name: row_id
44
+ for row_name, row_id in meta.Rows.items()}
45
+ return cls_instance
46
+
47
+
48
+ class Row:
49
+ def __init__(self, columns_map: Dict[str, ColumnMetadata]):
50
+ self.columns_map = columns_map
51
+ self.data: Dict[str, Result] = {}
52
+
53
+ def __getitem__(self, column_name: str) -> Result:
54
+ return self.data[column_name]
55
+
56
+ def __setitem__(self, column_name: str, result: Result):
57
+ if column_name not in self.columns_map:
58
+ raise ValueError(f"Column name '{column_name}' not found in columns_map.")
59
+ column_metadata = self.columns_map[column_name]
60
+ if not isinstance(result.value, column_metadata.type.value):
61
+ raise ValueError(f"Value {result.value} for column '{column_name}' is not of type {column_metadata.type.name}")
62
+ self.data[column_name] = result
63
+
64
+
65
+ @dataclass
66
+ class BaseResultTable(metaclass=ResultTableMeta):
67
+ results: Dict[str, Row] = field(default_factory=dict)
68
+
69
+ def as_dict(self) -> dict:
70
+ results_list = []
71
+ for row_name, row in self.results.items():
72
+ row_id = self.rows_map.get(row_name)
73
+ for column_name, result in row.data.items():
74
+ column_metadata = self.columns_map.get(column_name)
75
+ results_list.append({
76
+ "column_id": column_metadata.id,
77
+ "row_id": row_id,
78
+ "result": result.value,
79
+ "status": result.status.value
80
+ })
81
+
82
+ meta_info = {
83
+ "table_name": self.table_name,
84
+ "columns": {name: {"id": meta.id, "unit": meta.unit, "type": meta.type.name} for name, meta in self.columns_map.items()},
85
+ "rows": self.rows_map
86
+ }
87
+ return {
88
+ "meta": meta_info,
89
+ "results": results_list
90
+ }
91
+
92
+ def to_json(self) -> str:
93
+ dict_result = self.as_dict()
94
+ return json.dumps(dict_result, default=str)
95
+
96
+ def __getitem__(self, row_name: str) -> Row:
97
+ if row_name not in self.rows_map:
98
+ raise ValueError(f"Row name '{row_name}' not found in rows_map.")
99
+ if row_name not in self.results:
100
+ self.results[row_name] = Row(self.columns_map)
101
+ return self.results[row_name]
102
+
103
+
104
+ # Example of a specific result table with its own metadata
105
+ class LatencyResultTable(BaseResultTable):
106
+ class Meta:
107
+ table_name = "latency_percentile_write"
108
+ Columns = {
109
+ "latency": (1, "ms", ResultType.FLOAT),
110
+ "op_rate": (2, "ops", ResultType.INTEGER)
111
+ }
112
+ Rows = {
113
+ "mean": 1,
114
+ "p99": 2
115
+ }
116
+
117
+
118
+ # Client code example
119
+ if __name__ == "__main__":
120
+ class LatencyResultTable(BaseResultTable):
121
+ class Meta:
122
+ table_name = "latency_percentile_write"
123
+ Columns = {
124
+ "latency": (1, "ms", ResultType.FLOAT),
125
+ "op_rate": (2, "ops", ResultType.INTEGER)
126
+ }
127
+ Rows = {
128
+ "mean": 1,
129
+ "p99": 2
130
+ }
131
+
132
+ result_table = LatencyResultTable()
133
+
134
+ result_table["mean"]["latency"] = Result(value=1.1, status=Status.WARNING)
135
+ result_table["mean"]["op_rate"] = Result(value=59988, status=Status.ERROR)
136
+ result_table["p99"]["latency"] = Result(value=2.7, status=Status.PASS)
137
+ result_table["p99"]["op_rate"] = Result(value=59988, status=Status.WARNING)
138
+
139
+ from argus.client.sct.client import ArgusSCTClient
140
+
141
+ run_id = UUID("24e09748-bba4-47fd-a615-bf7ea2c425eb")
142
+ client = ArgusSCTClient(run_id, auth_token="UO+2GXL9XqSgcVJijWk5WnbPXPit5ot5nfkLAHAr7SaqROfSCWycabpp/wxyY8+I", base_url="http://localhost:5000")
143
+ client.submit_results(result_table)
@@ -1,8 +1,8 @@
1
1
  [tool.poetry]
2
2
  name = "argus-alm"
3
- version = "0.12.7"
3
+ version = "0.12.8"
4
4
  description = "Argus"
5
- authors = ["Alexey Kartashov <alexey.kartashov@scylladb.com>"]
5
+ authors = ["Alexey Kartashov <alexey.kartashov@scylladb.com>", "Łukasz Sójka <lukasz.sojka@scylladb.com>"]
6
6
  license = "Apache-2.0"
7
7
  repository = "https://github.com/scylladb/argus"
8
8
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes