argus-alm 0.12.4b2__py3-none-any.whl → 0.12.7__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.
@@ -7,8 +7,8 @@ from flask import (
7
7
  )
8
8
  from argus.backend.error_handlers import handle_api_exception
9
9
  from argus.backend.service.release_manager import ReleaseEditPayload, ReleaseManagerService
10
- from argus.backend.service.user import api_login_required, check_roles
11
- from argus.backend.models.web import UserRoles
10
+ from argus.backend.service.user import UserService, api_login_required, check_roles
11
+ from argus.backend.models.web import User, UserRoles
12
12
 
13
13
  bp = Blueprint('admin_api', __name__, url_prefix='/api/v1')
14
14
  LOGGER = logging.getLogger(__name__)
@@ -287,3 +287,68 @@ def quick_toggle_group_enabled():
287
287
  "status": "ok",
288
288
  "response": res
289
289
  }
290
+
291
+
292
+ @bp.route("/users", methods=["GET"])
293
+ @check_roles(UserRoles.Admin)
294
+ @api_login_required
295
+ def user_info():
296
+ result = UserService().get_users_privileged()
297
+
298
+ return {
299
+ "status": "ok",
300
+ "response": result
301
+ }
302
+
303
+
304
+ @bp.route("/user/<string:user_id>/email/set", methods=["POST"])
305
+ @check_roles(UserRoles.Admin)
306
+ @api_login_required
307
+ def user_change_email(user_id: str):
308
+ payload = get_payload(request)
309
+
310
+ user = User.get(id=user_id)
311
+ result = UserService().update_email(user=user, new_email=payload["newEmail"])
312
+
313
+ return {
314
+ "status": "ok",
315
+ "response": result
316
+ }
317
+
318
+
319
+ @bp.route("/user/<string:user_id>/delete", methods=["POST"])
320
+ @check_roles(UserRoles.Admin)
321
+ @api_login_required
322
+ def user_delete(user_id: str):
323
+ result = UserService().delete_user(user_id=user_id)
324
+
325
+ return {
326
+ "status": "ok",
327
+ "response": result
328
+ }
329
+
330
+
331
+ @bp.route("/user/<string:user_id>/password/set", methods=["POST"])
332
+ @check_roles(UserRoles.Admin)
333
+ @api_login_required
334
+ def user_change_password(user_id: str):
335
+ payload = get_payload(request)
336
+
337
+ user = User.get(id=user_id)
338
+ result = UserService().update_password(user=user, old_password="", new_password=payload["newPassword"], force=True)
339
+
340
+ return {
341
+ "status": "ok",
342
+ "response": result
343
+ }
344
+
345
+ @bp.route("/user/<string:user_id>/admin/toggle", methods=["POST"])
346
+ @check_roles(UserRoles.Admin)
347
+ @api_login_required
348
+ def user_toggle_admin(user_id: str):
349
+ result = UserService().toggle_admin(user_id=user_id)
350
+
351
+ return {
352
+ "status": "ok",
353
+ "response": result
354
+ }
@@ -14,6 +14,7 @@ from argus.backend.controller.testrun_api import bp as testrun_bp
14
14
  from argus.backend.controller.team import bp as team_bp
15
15
  from argus.backend.controller.view_api import bp as view_bp
16
16
  from argus.backend.service.argus_service import ArgusService, ScheduleUpdateRequest
17
+ from argus.backend.service.results_service import ResultsService
17
18
  from argus.backend.service.user import UserService, api_login_required
18
19
  from argus.backend.service.stats import ReleaseStatsCollector
19
20
  from argus.backend.models.web import ArgusRelease, ArgusGroup, ArgusTest, User, UserOauthToken
@@ -381,6 +382,19 @@ def test_info():
381
382
  "response": info
382
383
  }
383
384
 
385
+ @bp.route("/test-results", methods=["GET"])
386
+ @api_login_required
387
+ def test_results():
388
+ test_id = request.args.get("testId")
389
+ if not test_id:
390
+ raise Exception("No testId provided")
391
+ service = ResultsService()
392
+ info = service.get_results(test_id=UUID(test_id))
393
+
394
+ return {
395
+ "status": "ok",
396
+ "response": info
397
+ }
384
398
 
385
399
  @bp.route("/test_run/comment/get", methods=["GET"]) # TODO: remove
386
400
  @api_login_required
@@ -5,7 +5,7 @@ from flask import (
5
5
  )
6
6
  from werkzeug.security import check_password_hash
7
7
  from argus.backend.models.web import User
8
- from argus.backend.service.user import UserService, load_logged_in_user, login_required
8
+ from argus.backend.service.user import UserService, UserServiceException, load_logged_in_user, login_required
9
9
 
10
10
  bp = Blueprint('auth', __name__, url_prefix='/auth')
11
11
 
@@ -21,24 +21,26 @@ def login():
21
21
  session["csrf_token"] = token
22
22
 
23
23
  if request.method == 'POST':
24
- username = request.form["username"]
25
- password = request.form["password"]
26
- error = None
27
24
  try:
28
- user = User.get(username=username)
29
- except User.DoesNotExist:
30
- user = None
25
+ if "password" not in current_app.config.get("LOGIN_METHODS", []):
26
+ raise UserServiceException("Password Login is disabled")
27
+ username = request.form["username"]
28
+ password = request.form["password"]
31
29
 
32
- if not user:
33
- error = "User not found"
34
- elif not check_password_hash(user.password, password):
35
- error = "Incorrect Password"
30
+ try:
31
+ user: User = User.get(username=username)
32
+ except User.DoesNotExist:
33
+ raise UserServiceException("User not found")
34
+
35
+ if not check_password_hash(user.password, password):
36
+ raise UserServiceException("Incorrect Password")
36
37
 
37
- if not error:
38
38
  session.clear()
39
39
  session["user_id"] = str(user.id)
40
40
  session["csrf_token"] = token
41
- flash(error, category="error")
41
+ except UserServiceException as exc:
42
+ flash(next(iter(exc.args), "No message"), category="error")
43
+
42
44
  return redirect(url_for('main.home'))
43
45
 
44
46
  return render_template('auth/login.html.j2',
@@ -23,6 +23,15 @@ def submit_run(run_type: str):
23
23
  "response": result
24
24
  }
25
25
 
26
+ @bp.route("/testrun/<string:run_type>/<string:run_id>/get", methods=["GET"])
27
+ @api_login_required
28
+ def get_run(run_type: str, run_id: str):
29
+ result = ClientService().get_run(run_type=run_type, run_id=run_id)
30
+ return {
31
+ "status": "ok",
32
+ "response": result
33
+ }
34
+
26
35
 
27
36
  @bp.route("/testrun/<string:run_type>/<string:run_id>/heartbeat", methods=["POST"])
28
37
  @api_login_required
@@ -90,3 +99,14 @@ def run_finalize(run_type: str, run_id: str):
90
99
  "status": "ok",
91
100
  "response": result
92
101
  }
102
+
103
+
104
+ @bp.route("/testrun/<string:run_type>/<string:run_id>/submit_results", methods=["POST"])
105
+ @api_login_required
106
+ def submit_results(run_type: str, run_id: str):
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
+ }
@@ -152,6 +152,7 @@ def profile_oauth_github_callback():
152
152
  try:
153
153
  first_run_info = service.github_callback(req_code)
154
154
  except Exception as exc: # pylint: disable=broad-except
155
+ LOGGER.error("An error occured in callback", exc_info=True)
155
156
  flash(message=exc.args[0], category="error")
156
157
  return redirect(url_for("main.error", type=403))
157
158
  if first_run_info:
@@ -63,6 +63,17 @@ def test_run_activity(run_id: str):
63
63
  }
64
64
 
65
65
 
66
+
67
+ @bp.route("/run/<string:test_id>/<string:run_id>/fetch_results", methods=["GET"])
68
+ @api_login_required
69
+ 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
+ return {
72
+ "status": "ok",
73
+ "tables": tables
74
+ }
75
+
76
+
66
77
  @bp.route("/test/<string:test_id>/run/<string:run_id>/status/set", methods=["POST"])
67
78
  @api_login_required
68
79
  def set_testrun_status(test_id: str, run_id: str):
@@ -0,0 +1,63 @@
1
+ from cassandra.cqlengine import columns
2
+ from cassandra.cqlengine.models import Model
3
+ from cassandra.cqlengine.usertype import UserType
4
+ from enum import Enum
5
+
6
+
7
+ class Status(Enum):
8
+ PASS = 0
9
+ WARNING = 1
10
+ ERROR = 2
11
+
12
+
13
+ class ColumnMetadata(UserType):
14
+ name = columns.Ascii()
15
+ unit = columns.Text()
16
+ type = columns.Ascii()
17
+
18
+
19
+ class ArgusGenericResultMetadata(Model):
20
+ __table_name__ = "generic_result_metadata_v1"
21
+ test_id = columns.UUID(partition_key=True)
22
+ name = columns.Text(required=True, primary_key=True)
23
+ description = columns.Text()
24
+ columns_meta = columns.List(value_type=columns.UserDefinedType(ColumnMetadata))
25
+ rows_meta = columns.List(value_type=columns.Ascii())
26
+
27
+ def __init__(self, **kwargs):
28
+ kwargs["columns_meta"] = [ColumnMetadata(**col) for col in kwargs.pop('columns_meta', [])]
29
+ super().__init__(**kwargs)
30
+
31
+ def update_if_changed(self, new_data: dict) -> None:
32
+ """
33
+ Updates table metadata if changed column/description or new rows were added.
34
+ See that rows can only be added, not removed once was sent.
35
+ Columns may be removed, but data in results table persists.
36
+ """
37
+ updated = False
38
+ for field, value in new_data.items():
39
+ if field == "columns_meta":
40
+ value = [ColumnMetadata(**col) for col in value]
41
+ elif field == "rows_meta":
42
+ added_rows = []
43
+ for row in value:
44
+ if row not in self.rows_meta:
45
+ added_rows.append(row)
46
+ value = self.rows_meta + added_rows
47
+ if getattr(self, field) != value:
48
+ setattr(self, field, value)
49
+ updated = True
50
+
51
+ if updated:
52
+ self.save()
53
+
54
+ class ArgusGenericResultData(Model):
55
+ __table_name__ = "generic_result_data_v1"
56
+ test_id = columns.UUID(partition_key=True)
57
+ name = columns.Text(partition_key=True)
58
+ run_id = columns.UUID(primary_key=True)
59
+ column = columns.Ascii(primary_key=True, index=True)
60
+ row = columns.Ascii(primary_key=True, index=True)
61
+ sut_timestamp = columns.DateTime() # for sorting
62
+ value = columns.Double()
63
+ status = columns.Ascii()
@@ -6,6 +6,8 @@ from cassandra.cqlengine.usertype import UserType
6
6
  from cassandra.cqlengine import columns
7
7
  from cassandra.util import uuid_from_time, unix_time_from_uuid1 # pylint: disable=no-name-in-module
8
8
 
9
+ from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
10
+
9
11
 
10
12
  def uuid_now():
11
13
  return uuid_from_time(datetime.utcnow())
@@ -377,6 +379,8 @@ USED_MODELS: list[Model] = [
377
379
  ArgusScheduleAssignee,
378
380
  ArgusScheduleGroup,
379
381
  ArgusScheduleTest,
382
+ ArgusGenericResultMetadata,
383
+ ArgusGenericResultData,
380
384
  ]
381
385
 
382
386
  USED_TYPES: list[UserType] = [
@@ -105,16 +105,16 @@ class PluginModelBase(Model):
105
105
  assignees = ArgusScheduleAssignee.filter(
106
106
  schedule_id=schedule.id
107
107
  ).all()
108
- assignees_uuids.append(*[assignee.assignee for assignee in assignees])
108
+ assignees_uuids.extend([assignee.assignee for assignee in assignees])
109
109
 
110
110
  return assignees_uuids[0] if len(assignees_uuids) > 0 else None
111
111
 
112
112
  @classmethod
113
- def get_jobs_assigned_to_user(cls, user: User):
113
+ def get_jobs_assigned_to_user(cls, user_id: str | UUID):
114
114
  cluster = ScyllaCluster.get()
115
115
  query = cluster.prepare("SELECT build_id, start_time, release_id, group_id, assignee, "
116
116
  f"test_id, id, status, investigation_status, build_job_url, scylla_version FROM {cls.table_name()} WHERE assignee = ?")
117
- rows = cluster.session.execute(query=query, parameters=(user.id,))
117
+ rows = cluster.session.execute(query=query, parameters=(user_id,))
118
118
 
119
119
  return list(rows)
120
120
 
@@ -213,6 +213,8 @@ class PluginModelBase(Model):
213
213
  def finish_run(self, payload: dict = None):
214
214
  raise NotImplementedError()
215
215
 
216
+ def sut_timestamp(self) -> float:
217
+ raise NotImplementedError()
216
218
 
217
219
  class PluginInfoBase:
218
220
  # pylint: disable=too-few-public-methods
@@ -109,6 +109,7 @@ class SCTTestRun(PluginModelBase):
109
109
  stress_cmd = columns.Text()
110
110
 
111
111
  histograms = columns.List(value_type=columns.Map(key_type=columns.Text(), value_type=columns.UserDefinedType(user_type=PerformanceHDRHistogram)))
112
+ test_method = columns.Ascii()
112
113
 
113
114
  @classmethod
114
115
  def _stats_query(cls) -> str:
@@ -199,6 +200,7 @@ class SCTTestRun(PluginModelBase):
199
200
 
200
201
  run.config_files = req.sct_config.get("config_files")
201
202
  run.region_name = regions
203
+ run.test_method = req.sct_config.get("test_method")
202
204
  run.save()
203
205
 
204
206
  return run
@@ -249,6 +251,13 @@ class SCTTestRun(PluginModelBase):
249
251
 
250
252
  self._collect_event_message(event, event_message)
251
253
 
254
+ def sut_timestamp(self) -> float:
255
+ """converts scylla-server date to timestamp and adds revision in subseconds precision to diffirentiate
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()
259
+ + int(scylla_package.revision_id, 16) % 1000000 / 1000000)
260
+
252
261
 
253
262
  class SCTJunitReports(Model):
254
263
  test_id = columns.UUID(primary_key=True, partition_key=True, required=True)
@@ -459,8 +459,13 @@ class ArgusService:
459
459
  self.update_schedule_comment({"newComment": comment, "releaseId": test.release_id, "groupId": test.group_id, "testId": test.id})
460
460
 
461
461
  schedule_assignee: ArgusScheduleAssignee = ArgusScheduleAssignee.get(schedule_id=schedule_id)
462
- schedule_assignee.assignee = assignee
463
- schedule_assignee.save()
462
+ new_assignee = ArgusScheduleAssignee()
463
+ new_assignee.assignee = assignee
464
+ new_assignee.release_id = schedule_assignee.release_id
465
+ new_assignee.schedule_id = schedule_assignee.schedule_id
466
+ new_assignee.save()
467
+ schedule_assignee.delete()
468
+
464
469
  return True
465
470
 
466
471
  def delete_schedule(self, payload: dict) -> dict:
@@ -493,11 +498,16 @@ class ArgusService:
493
498
  full_schedule["assignees"] = [assignee.assignee for assignee in assignees]
494
499
 
495
500
  if len(assignees) > 0:
496
- schedule_user = User.get(id=assignees[0].assignee)
501
+ try:
502
+ schedule_user = User.get(id=assignees[0].assignee)
503
+ except User.DoesNotExist:
504
+ schedule_user = User()
505
+ schedule_user.id = assignees[0].assignee
506
+ LOGGER.warning("Deleting orphaned user assignments")
497
507
  service = TestRunService()
498
508
 
499
509
  for model in all_plugin_models():
500
- for run in model.get_jobs_assigned_to_user(schedule_user):
510
+ for run in model.get_jobs_assigned_to_user(schedule_user.id):
501
511
  if run["release_id"] != release.id:
502
512
  continue
503
513
  if run["test_id"] not in full_schedule["tests"]:
@@ -644,7 +654,7 @@ class ArgusService:
644
654
  today = datetime.datetime.now()
645
655
  validity_period = today - datetime.timedelta(days=current_app.config.get("JOB_VALIDITY_PERIOD_DAYS", 30))
646
656
  for plugin in all_plugin_models():
647
- for run in plugin.get_jobs_assigned_to_user(user=user):
657
+ for run in plugin.get_jobs_assigned_to_user(user_id=user.id):
648
658
  if run["start_time"] >= validity_period:
649
659
  yield run
650
660
 
@@ -20,9 +20,9 @@ class ArgusTestsMonitor(ABC):
20
20
 
21
21
  def __init__(self) -> None:
22
22
  self._cluster = ScyllaCluster.get()
23
- self._existing_releases = list(ArgusRelease.all())
24
- self._existing_groups = list(ArgusGroup.all())
25
- self._existing_tests = list(ArgusTest.all())
23
+ self._existing_releases = list(ArgusRelease.objects().limit(None))
24
+ self._existing_groups = list(ArgusGroup.objects().limit(None))
25
+ self._existing_tests = list(ArgusTest.objects().limit(None))
26
26
  self._filtered_groups: list[str] = self.BUILD_SYSTEM_FILTERED_PREFIXES
27
27
 
28
28
  def create_release(self, release_name):
@@ -1,5 +1,6 @@
1
1
  from uuid import UUID
2
2
  from argus.backend.db import ScyllaCluster
3
+ from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
3
4
  from argus.backend.plugins.core import PluginModelBase
4
5
  from argus.backend.plugins.loader import AVAILABLE_PLUGINS
5
6
  from argus.backend.util.enums import TestStatus
@@ -25,6 +26,14 @@ class ClientService:
25
26
  model = self.get_model(run_type)
26
27
  model.submit_run(request_data=request_data)
27
28
  return "Created"
29
+
30
+ def get_run(self, run_type: str, run_id: str):
31
+ model = self.get_model(run_type)
32
+ try:
33
+ run = model.get(id=run_id)
34
+ except model.DoesNotExist:
35
+ return None
36
+ return run
28
37
 
29
38
  def heartbeat(self, run_type: str, run_id: str) -> int:
30
39
  model = self.get_model(run_type)
@@ -69,3 +78,24 @@ class ClientService:
69
78
  run.save()
70
79
 
71
80
  return "Finalized"
81
+
82
+ def submit_results(self, run_type: str, run_id: str, results: dict) -> str:
83
+ model = self.get_model(run_type)
84
+ run = model.load_test_run(UUID(run_id))
85
+ existing_table = ArgusGenericResultMetadata.objects(test_id=run.test_id, name=results["meta"]["name"]).first()
86
+ if existing_table:
87
+ existing_table.update_if_changed(results["meta"])
88
+ else:
89
+ ArgusGenericResultMetadata(test_id=run.test_id, **results["meta"]).save()
90
+ if results.get("sut_timestamp", 0) == 0:
91
+ results["sut_timestamp"] = run.sut_timestamp() # automatic sut_timestamp
92
+ table_name = results["meta"]["name"]
93
+ sut_timestamp = results["sut_timestamp"]
94
+ for cell in results["results"]:
95
+ ArgusGenericResultData(test_id=run.test_id,
96
+ run_id=run.id,
97
+ name=table_name,
98
+ sut_timestamp=sut_timestamp,
99
+ **cell
100
+ ).save()
101
+ return "Submitted"
@@ -220,7 +220,9 @@ class JenkinsService:
220
220
  def build_job(self, build_id: str, params: dict, user_override: str = None):
221
221
  queue_number = self._jenkins.build_job(build_id, {
222
222
  **params,
223
- self.RESERVED_PARAMETER_NAME: g.user.username if not user_override else user_override
223
+ # use the user's email as the default value for the requested by user parameter,
224
+ # so it would align with how SCT default works, on runs not trigger by argus
225
+ self.RESERVED_PARAMETER_NAME: g.user.email.split('@')[0] if not user_override else user_override
224
226
  })
225
227
  return queue_number
226
228
 
@@ -0,0 +1,140 @@
1
+ import copy
2
+ import logging
3
+ import math
4
+ from typing import List, Dict, Any
5
+ from uuid import UUID
6
+
7
+ from argus.backend.db import ScyllaCluster
8
+ from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+
12
+ default_options = {
13
+ "scales": {
14
+ "y": {
15
+ "beginAtZero": True,
16
+ "title": {
17
+ "display": True,
18
+ "text": ''
19
+ }
20
+ },
21
+ "x": {
22
+ "type": "time",
23
+ "time": {
24
+ "unit": "day",
25
+ "displayFormats": {
26
+ "day": "yyyy-MM-dd",
27
+ },
28
+ },
29
+ "title": {
30
+ "display": True,
31
+ "text": 'SUT Date'
32
+ }
33
+ },
34
+ },
35
+ "elements": {
36
+ "line": {
37
+ "tension": .1,
38
+ }
39
+ },
40
+ "plugins": {
41
+ "legend": {
42
+ "position": 'top',
43
+ },
44
+ "title": {
45
+ "display": True,
46
+ "text": ''
47
+ }
48
+ }
49
+ }
50
+
51
+ colors = [
52
+ 'rgba(255, 0, 0, 1.0)', # Red
53
+ 'rgba(0, 255, 0, 1.0)', # Green
54
+ 'rgba(0, 0, 255, 1.0)', # Blue
55
+ 'rgba(0, 255, 255, 1.0)', # Cyan
56
+ 'rgba(255, 0, 255, 1.0)', # Magenta
57
+ 'rgba(255, 255, 0, 1.0)', # Yellow
58
+ 'rgba(255, 165, 0, 1.0)', # Orange
59
+ 'rgba(128, 0, 128, 1.0)', # Purple
60
+ 'rgba(50, 205, 50, 1.0)', # Lime
61
+ 'rgba(255, 192, 203, 1.0)', # Pink
62
+ 'rgba(0, 128, 128, 1.0)', # Teal
63
+ 'rgba(165, 42, 42, 1.0)', # Brown
64
+ 'rgba(0, 0, 128, 1.0)', # Navy
65
+ 'rgba(128, 128, 0, 1.0)', # Olive
66
+ 'rgba(255, 127, 80, 1.0)' # Coral
67
+ ]
68
+
69
+
70
+ def get_sorted_data_for_column_and_row(data: List[Dict[str, Any]], column: str, row: str) -> List[Dict[str, Any]]:
71
+ return sorted([{"x": entry.sut_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'),
72
+ "y": entry.value,
73
+ "id": entry.run_id}
74
+ for entry in data if entry['column'] == column and entry['row'] == row],
75
+ key=lambda x: x['x'])
76
+
77
+
78
+ def get_min_max_y(datasets: List[Dict[str, Any]]) -> (float, float):
79
+ """0.5 - 1.5 of min/max of 50% results"""
80
+ y = [entry['y'] for dataset in datasets for entry in dataset['data']]
81
+ if not y:
82
+ return 0, 0
83
+ sorted_y = sorted(y)
84
+ lower_percentile_index = int(0.25 * len(sorted_y))
85
+ upper_percentile_index = int(0.75 * len(sorted_y)) - 1
86
+ y_min = sorted_y[lower_percentile_index]
87
+ y_max = sorted_y[upper_percentile_index]
88
+ return math.floor(0.5 * y_min), math.ceil(1.5 * y_max)
89
+
90
+
91
+ def round_datasets_to_min_max(datasets: List[Dict[str, Any]], min_y: float, max_y: float) -> List[Dict[str, Any]]:
92
+ """Round values to min/max and provide original value for tooltip"""
93
+ for dataset in datasets:
94
+ for entry in dataset['data']:
95
+ val = entry['y']
96
+ if val > max_y:
97
+ entry['y'] = max_y
98
+ entry['ori'] = val
99
+ elif val < min_y:
100
+ entry['y'] = min_y
101
+ entry['ori'] = val
102
+ return datasets
103
+
104
+
105
+ def create_chartjs(table, data):
106
+ graphs = []
107
+ for column in table.columns_meta:
108
+ datasets = [
109
+ {"label": row,
110
+ "borderColor": colors[idx % len(colors)],
111
+ "borderWidth": 3,
112
+ "showLine": True,
113
+ "data": get_sorted_data_for_column_and_row(data, column.name, row)} for idx, row in enumerate(table.rows_meta)]
114
+ min_y, max_y = get_min_max_y(datasets)
115
+ datasets = round_datasets_to_min_max(datasets, min_y, max_y)
116
+ if not min_y + max_y:
117
+ # filter out those without data
118
+ continue
119
+ options = copy.deepcopy(default_options)
120
+ options["plugins"]["title"]["text"] = f"{table.name} - {column.name}"
121
+ options["scales"]["y"]["title"]["text"] = f"[{column.unit}]"
122
+ options["scales"]["y"]["min"] = min_y
123
+ options["scales"]["y"]["max"] = max_y
124
+ graphs.append({"options": options, "data":
125
+ {"datasets": datasets}})
126
+ return graphs
127
+
128
+
129
+ class ResultsService:
130
+
131
+ def __init__(self, database_session=None):
132
+ self.session = database_session if database_session else ScyllaCluster.get_session()
133
+
134
+ def get_results(self, test_id: UUID):
135
+ 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()
139
+ graphs.extend(create_chartjs(table, data))
140
+ return graphs
@@ -13,6 +13,7 @@ from flask import g
13
13
  from cassandra.query import BatchStatement, ConsistencyLevel
14
14
  from cassandra.cqlengine.query import BatchQuery
15
15
  from argus.backend.db import ScyllaCluster
16
+ from argus.backend.models.result import ArgusGenericResultMetadata
16
17
 
17
18
  from argus.backend.models.web import (
18
19
  ArgusEvent,
@@ -222,7 +223,7 @@ class TestRunService:
222
223
  mentions = set(mentions)
223
224
  for potential_mention in re.findall(self.RE_MENTION, message_stripped):
224
225
  if user := User.exists_by_name(potential_mention.lstrip("@")):
225
- mentions.add(user)
226
+ mentions.add(user) if user.id != g.user.id else None
226
227
 
227
228
  test: ArgusTest = ArgusTest.get(id=test_id)
228
229
  plugin = self.get_plugin(test.plugin_name)
@@ -306,6 +307,29 @@ class TestRunService:
306
307
  }
307
308
  return response
308
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
+
309
333
  def submit_github_issue(self, issue_url: str, test_id: UUID, run_id: UUID):
310
334
  user_tokens = UserOauthToken.filter(user_id=g.user.id).all()
311
335
  token = None
@@ -17,6 +17,9 @@ from argus.backend.models.web import User, UserOauthToken, UserRoles, WebFileSto
17
17
  from argus.backend.util.common import FlaskView
18
18
 
19
19
 
20
+ class UserServiceException(Exception):
21
+ pass
22
+
20
23
  class GithubOrganizationMissingError(Exception):
21
24
  pass
22
25
 
@@ -29,6 +32,8 @@ class UserService:
29
32
 
30
33
  @staticmethod
31
34
  def check_roles(roles: list[UserRoles] | UserRoles, user: User) -> bool:
35
+ if not user:
36
+ return False
32
37
  if isinstance(roles, str):
33
38
  return roles in user.roles
34
39
  elif isinstance(roles, list):
@@ -38,6 +43,8 @@ class UserService:
38
43
  return False
39
44
 
40
45
  def github_callback(self, req_code: str) -> dict | None:
46
+ if "gh" not in current_app.config.get("LOGIN_METHODS", []):
47
+ raise UserServiceException("Github Login is disabled")
41
48
  # pylint: disable=too-many-locals
42
49
  oauth_response = requests.post(
43
50
  "https://github.com/login/oauth/access_token",
@@ -90,7 +97,10 @@ class UserService:
90
97
  except User.DoesNotExist:
91
98
  user = User()
92
99
  user.username = user_info.get("login")
93
- user.email = email_info[-1].get("email")
100
+ # pick only scylladb.com emails
101
+ scylla_email = next(iter([email.get("email") for email in email_info if email.get("email").endswith("@scylladb.com")]), None)
102
+ primary_email = next(iter([email.get("email") for email in email_info if email.get("primary") and email.get("verified")]), None)
103
+ user.email = scylla_email or primary_email
94
104
  user.full_name = user_info.get("name", user_info.get("login"))
95
105
  user.registration_date = datetime.utcnow()
96
106
  user.roles = ["ROLE_USER"]
@@ -137,6 +147,15 @@ class UserService:
137
147
  def get_users(self) -> dict:
138
148
  users = User.all()
139
149
  return {str(user.id): user.to_json() for user in users}
150
+
151
+ def get_users_privileged(self) -> dict:
152
+ users = User.all()
153
+ users = {str(user.id): dict(user.items()) for user in users}
154
+ for user in users.values():
155
+ user.pop("password")
156
+ user.pop("api_token")
157
+
158
+ return users
140
159
 
141
160
  def generate_token(self, user: User):
142
161
  token_digest = f"{user.username}-{int(time())}-{base64.encodebytes(os.urandom(128)).decode(encoding='utf-8')}"
@@ -150,13 +169,51 @@ class UserService:
150
169
  user.email = new_email
151
170
  user.save()
152
171
 
153
- def update_password(self, user: User, old_password: str, new_password: str):
154
- if check_password_hash(user.password, old_password):
155
- raise Exception("Incorrect old password")
172
+ return True
173
+
174
+ def toggle_admin(self, user_id: str):
175
+ user: User = User.get(id=user_id)
176
+
177
+ if user.id == g.user.id:
178
+ raise UserServiceException("Cannot toggle admin role from yourself.")
179
+
180
+ is_admin = UserService.check_roles(UserRoles.Admin, user)
181
+
182
+ if is_admin:
183
+ user.roles.remove(UserRoles.Admin)
184
+ else:
185
+ user.set_as_admin()
186
+
187
+ user.save()
188
+ return True
189
+
190
+ def delete_user(self, user_id: str):
191
+ user: User = User.get(id=user_id)
192
+ if user.id == g.user.id:
193
+ raise UserServiceException("Cannot delete user that you are logged in as.")
194
+
195
+ if user.is_admin():
196
+ raise UserServiceException("Cannot delete admin users. Unset admin flag before deleting")
197
+
198
+ user.delete()
199
+
200
+ return True
201
+
202
+ def update_password(self, user: User, old_password: str, new_password: str, force = False):
203
+ if not check_password_hash(user.password, old_password) and not force:
204
+ raise UserServiceException("Incorrect old password")
205
+
206
+ if not new_password:
207
+ raise UserServiceException("Empty new password")
208
+
209
+ if len(new_password) < 5:
210
+ raise UserServiceException("New password is too short")
156
211
 
157
212
  user.password = generate_password_hash(new_password)
158
213
  user.save()
159
214
 
215
+ return True
216
+
160
217
  def update_name(self, user: User, new_name: str):
161
218
  user.full_name = new_name
162
219
  user.save()
@@ -110,9 +110,9 @@ class UserViewService:
110
110
  search_func = facet_wrapper(query_func=search_func, facet_query=value, facet_type=facet)
111
111
 
112
112
 
113
- all_tests = ArgusTest.all()
114
- all_releases = ArgusRelease.all()
115
- all_groups = ArgusGroup.all()
113
+ all_tests = ArgusTest.objects().limit(None)
114
+ all_releases = ArgusRelease.objects().limit(None)
115
+ all_groups = ArgusGroup.objects().limit(None)
116
116
  release_by_id = {release.id: partial(self.index_mapper, type="release")(release) for release in all_releases}
117
117
  group_by_id = {group.id: partial(self.index_mapper, type="group")(group) for group in all_groups}
118
118
  index = [self.index_mapper(t) for t in all_tests]
@@ -10,8 +10,10 @@ LOGGER = logging.getLogger(__name__)
10
10
  class Config:
11
11
  CONFIG = None
12
12
  CONFIG_PATHS = [
13
- Path("./config/argus_web.yaml"),
13
+ Path(__file__).parents[3] / "config" / "argus_web.yaml",
14
14
  Path("argus_web.yaml"),
15
+ Path("../config/argus_web.yaml"),
16
+
15
17
  ]
16
18
 
17
19
  @classmethod
@@ -3,6 +3,7 @@ import logging
3
3
  from json.encoder import JSONEncoder
4
4
  from uuid import UUID
5
5
 
6
+ from flask.json.provider import DefaultJSONProvider
6
7
  import cassandra.cqlengine.usertype as ut
7
8
  import cassandra.cqlengine.models as m
8
9
 
@@ -22,3 +23,19 @@ class ArgusJSONEncoder(JSONEncoder):
22
23
  return o.strftime("%Y-%m-%dT%H:%M:%SZ")
23
24
  case _:
24
25
  return super().default(o)
26
+
27
+
28
+ class ArgusJSONProvider(DefaultJSONProvider):
29
+
30
+ def default(self, o):
31
+ match o:
32
+ case UUID():
33
+ return str(o)
34
+ case ut.UserType():
35
+ return dict(o.items())
36
+ case m.Model():
37
+ return dict(o.items())
38
+ case datetime():
39
+ return o.strftime("%Y-%m-%dT%H:%M:%SZ")
40
+ case _:
41
+ return super().default(o)
argus/client/base.py CHANGED
@@ -7,6 +7,7 @@ from uuid import UUID
7
7
  import requests
8
8
 
9
9
  from argus.backend.util.enums import TestStatus
10
+ from argus.client.generic_result import GenericResultTable
10
11
  from argus.client.sct.types import LogLink
11
12
 
12
13
  JSON = dict[str, Any] | list[Any] | int | str | float | bool | Type[None]
@@ -23,11 +24,14 @@ class ArgusClient:
23
24
  class Routes():
24
25
  # pylint: disable=too-few-public-methods
25
26
  SUBMIT = "/testrun/$type/submit"
27
+ GET = "/testrun/$type/$id/get"
26
28
  HEARTBEAT = "/testrun/$type/$id/heartbeat"
27
29
  GET_STATUS = "/testrun/$type/$id/get_status"
28
30
  SET_STATUS = "/testrun/$type/$id/set_status"
29
31
  SET_PRODUCT_VERSION = "/testrun/$type/$id/update_product_version"
30
32
  SUBMIT_LOGS = "/testrun/$type/$id/logs/submit"
33
+ SUBMIT_RESULTS = "/testrun/$type/$id/submit_results"
34
+ FETCH_RESULTS = "/testrun/$type/$id/fetch_results"
31
35
  FINALIZE = "/testrun/$type/$id/finalize"
32
36
 
33
37
  def __init__(self, auth_token: str, base_url: str, api_version="v1") -> None:
@@ -122,7 +126,23 @@ class ArgusClient:
122
126
  **self.generic_body,
123
127
  **run_body
124
128
  })
125
-
129
+
130
+ def get_run(self, run_type: str = None, run_id: UUID | str = None) -> requests.Response:
131
+
132
+ if not run_type and hasattr(self, "test_type"):
133
+ run_type = self.test_type
134
+
135
+ if not run_id and hasattr(self, "run_id"):
136
+ run_id = self.run_id
137
+
138
+ if not (run_type and run_id):
139
+ raise ValueError("run_type and run_id must be set in func params or object attributes")
140
+
141
+ response = self.get(endpoint=self.Routes.GET, location_params={"type": run_type, "id": run_id })
142
+ self.check_response(response)
143
+
144
+ return response.json()["response"]
145
+
126
146
  def get_status(self, run_type: str = None, run_id: UUID = None) -> TestStatus:
127
147
  if not run_type and hasattr(self, "test_type"):
128
148
  run_type = self.test_type
@@ -190,3 +210,16 @@ class ArgusClient:
190
210
  }
191
211
  )
192
212
  self.check_response(response)
213
+
214
+ def submit_results(self, result: GenericResultTable) -> None:
215
+ response = self.post(
216
+ endpoint=self.Routes.SUBMIT_RESULTS,
217
+ location_params={"type": self.test_type, "id": str(self.run_id)},
218
+ body={
219
+ **self.generic_body,
220
+ "run_id": str(self.run_id),
221
+ ** result.as_dict(),
222
+ }
223
+ )
224
+ self.check_response(response)
225
+
@@ -0,0 +1,99 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum, auto
3
+ from typing import Union
4
+
5
+
6
+ class Status(Enum):
7
+ PASS = auto()
8
+ WARNING = auto()
9
+ ERROR = auto()
10
+
11
+ def __str__(self):
12
+ return self.name
13
+
14
+
15
+ class ResultType(Enum):
16
+ INTEGER = auto()
17
+ FLOAT = auto()
18
+ DURATION = auto()
19
+
20
+ def __str__(self):
21
+ return self.name
22
+
23
+
24
+ @dataclass
25
+ class ColumnMetadata:
26
+ name: str
27
+ unit: str
28
+ type: ResultType
29
+
30
+ def as_dict(self) -> dict:
31
+ return {
32
+ "name": self.name,
33
+ "unit": self.unit,
34
+ "type": str(self.type)
35
+ }
36
+
37
+
38
+ class ResultTableMeta(type):
39
+ def __new__(cls, name, bases, dct):
40
+ cls_instance = super().__new__(cls, name, bases, dct)
41
+ meta = dct.get('Meta')
42
+
43
+ if meta:
44
+ cls_instance.name = meta.name
45
+ cls_instance.description = meta.description
46
+ cls_instance.columns = meta.Columns
47
+ cls_instance.column_names = {column.name for column in cls_instance.columns}
48
+ cls_instance.rows = []
49
+ return cls_instance
50
+
51
+
52
+ @dataclass
53
+ class Cell:
54
+ column: str
55
+ row: str
56
+ value: Union[int, float, str]
57
+ status: Status
58
+
59
+ def as_dict(self) -> dict:
60
+ return {
61
+ "column": self.column,
62
+ "row": self.row,
63
+ "value": self.value,
64
+ "status": str(self.status)
65
+ }
66
+
67
+
68
+ @dataclass
69
+ class GenericResultTable(metaclass=ResultTableMeta):
70
+ """
71
+ Base class for all Generic Result Tables in Argus. Use it as a base class for your result table.
72
+ """
73
+ sut_timestamp: int = 0 # automatic timestamp based on SUT version. Works only with SCT and refers to Scylla version.
74
+ sut_details: str = ""
75
+ results: list[Cell] = field(default_factory=list)
76
+
77
+ def as_dict(self) -> dict:
78
+ rows = []
79
+ for result in self.results:
80
+ if result.row not in rows:
81
+ rows.append(result.row)
82
+
83
+ meta_info = {
84
+ "name": self.name,
85
+ "description": self.description,
86
+ "columns_meta": [column.as_dict() for column in self.columns],
87
+ "rows_meta": rows
88
+ }
89
+ return {
90
+ "meta": meta_info,
91
+ "sut_timestamp": self.sut_timestamp,
92
+ "sut_details": self.sut_details,
93
+ "results": [result.as_dict() for result in self.results]
94
+ }
95
+
96
+ def add_result(self, column: str, row: str, value: Union[int, float, str], status: Status):
97
+ if column not in self.column_names:
98
+ raise ValueError(f"Column {column} not found in the table")
99
+ self.results.append(Cell(column=column, row=row, value=value, status=status))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: argus-alm
3
- Version: 0.12.4b2
3
+ Version: 0.12.7
4
4
  Summary: Argus
5
5
  Home-page: https://github.com/scylladb/argus
6
6
  License: Apache-2.0
@@ -4,24 +4,25 @@ argus/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  argus/backend/cli.py,sha256=fWSS1m0mhQeCwfH58Qfs4Cicxc95IKi9vwmQn3SUYs0,1346
5
5
  argus/backend/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  argus/backend/controller/admin.py,sha256=2z29RX7ZQO_VTklSKH9RrEj-Ag2SsvyOaIzWDKr0ahQ,575
7
- argus/backend/controller/admin_api.py,sha256=I2eQbU5f5rlQeL7eI4t9mRoxM3SJ5O-6fqLsHf0wcEU,6975
8
- argus/backend/controller/api.py,sha256=HvjpR4ug5NXiTdBlrFTBeq57YQ5Vl_jca-IT35sAuas,14203
9
- argus/backend/controller/auth.py,sha256=nwF_5uZLxPOCLm2ljLyw2OxGur6fmPM7WAAoGJLI_kk,2030
10
- argus/backend/controller/client_api.py,sha256=SM6HRlnoguxkPrqHzm42b5JsoQ2b8-1hTtfLWe832hc,3007
11
- argus/backend/controller/main.py,sha256=gYA0pqUBlLhpedkOetUX0V82qfe489frOEHPEtM-iHQ,8992
7
+ argus/backend/controller/admin_api.py,sha256=lj5g6rdoKN9X13H9hXmKYx_-9tftt6HSftiNFLCr_kY,8567
8
+ argus/backend/controller/api.py,sha256=3TmNyrQXG4AfsHBAGFpUcDemaOlU_ePyx1S6qZsVABs,14613
9
+ argus/backend/controller/auth.py,sha256=rGKgqqjfiZOnoOEpthg5JTd1BuotcEk0S95z_USrgN4,2291
10
+ argus/backend/controller/client_api.py,sha256=IKDIX4rVZgSp5g35ptR1Yx00bAWXm0aRGt6vAroDV74,3646
11
+ argus/backend/controller/main.py,sha256=EXrwvGq2TyebT8a54Ojkxq-o5_QOhm0w51M53G0h_n0,9060
12
12
  argus/backend/controller/notification_api.py,sha256=wz7V4nE6Mxclpq78P8gNnCyeQ7xA9BBJjZ-dPhLLd2I,1964
13
13
  argus/backend/controller/notifications.py,sha256=zMSJln72BGU6Q_nQvJesMnuvJ57Ucbov4M2ZI-37Bxo,290
14
14
  argus/backend/controller/team.py,sha256=G6LdIBaYgfG0Qr4RhNQ53MZVdh4wcuotsIIpFwhTJ3w,3101
15
15
  argus/backend/controller/team_ui.py,sha256=B7N1_Kzl6Rac8BV3FbKj55pGAS_dht47rYhAi94PC8A,589
16
- argus/backend/controller/testrun_api.py,sha256=92VI_FngMxr9SpKMhlEdSzambvbL4dpMtfx26Z3_Eso,12264
16
+ argus/backend/controller/testrun_api.py,sha256=nJZz2VhyNUWMyHdOmnK-BbxT7ifMLrMPZl8qh9tjVpM,12571
17
17
  argus/backend/controller/view_api.py,sha256=rI7LwcS7keK37nYx76D9StFV_rLHcNkHan8OhFgBrhM,4106
18
18
  argus/backend/db.py,sha256=bBiraYD05Qex28yZHjSP1bRlcMsc6oTYGt792zXmaHo,4101
19
19
  argus/backend/error_handlers.py,sha256=IEjz7Vzfldv1PTOeHrpRWmRsgBrHtAW0PXHUJZDovAE,480
20
20
  argus/backend/events/event_processors.py,sha256=bsmBayiXvlGn3aqiT2z9WgwnVBRtn2cRqkgn4pLodck,1291
21
21
  argus/backend/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- argus/backend/models/web.py,sha256=nl9sCOaPT53UKcZwguQB1DVm523Scgw7d2Rs4xaC9x4,12943
22
+ argus/backend/models/result.py,sha256=WNB7nfjLBRwbvWcGW8Y1Co4gWYYwvFeHZb9CW5mw1PM,2216
23
+ argus/backend/models/web.py,sha256=4K1Gj70nugmuW3sv0Sv5M_sVSmEhfJgxRE670qChGzo,13095
23
24
  argus/backend/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- argus/backend/plugins/core.py,sha256=kMp2zrEcgAZP0D4yw_dzY5uD8DcC19CoZ_E7urH0f0k,8608
25
+ argus/backend/plugins/core.py,sha256=UsrK8oWyhpDSLEcnqAQsEqYVUIX0eCQB862Em7GMQLo,8690
25
26
  argus/backend/plugins/driver_matrix_tests/controller.py,sha256=GdPpProzsVXQw8A4h2IS8inUPdr_Q4IN93i6ocOThS8,2213
26
27
  argus/backend/plugins/driver_matrix_tests/model.py,sha256=Wqot8rOYqroYV7cn60ltlarnuIuYrL2qyrk13NwLeT4,15419
27
28
  argus/backend/plugins/driver_matrix_tests/plugin.py,sha256=72ESU7s8C6ovVMfJTlYwtaokdvRp_HJF1_czm1UMhKg,745
@@ -36,39 +37,41 @@ argus/backend/plugins/sct/controller.py,sha256=NF11JLoUJ13whghlxRrVex9rLMgFtlkcz
36
37
  argus/backend/plugins/sct/plugin.py,sha256=_sOMcXLoFfeG9jwj_t48C4IFvY87juK8ApR6tfSw6q4,1007
37
38
  argus/backend/plugins/sct/resource_setup.py,sha256=hwfAOu-oKOH42tjtzJhiqwq_MtUE9_HevoFyql8JKqY,10120
38
39
  argus/backend/plugins/sct/service.py,sha256=ygAL85BkyyovJ1xHktlCQJdJS8CrerJZ_Tbr3EXqsg4,22021
39
- argus/backend/plugins/sct/testrun.py,sha256=NopC3Gnzf2m65S_E4DedAlgnbFvyaMQH5aLJip6ZPrE,9660
40
+ argus/backend/plugins/sct/testrun.py,sha256=vd44G99mqdR4mAb4hEou48xZN5r1OTg8QTVaawGx9p8,10264
40
41
  argus/backend/plugins/sct/types.py,sha256=Gw1y4iqYguqNqTh_GopLDFho8vuGaOGuK7fjaHYhAOQ,1326
41
42
  argus/backend/plugins/sct/udt.py,sha256=V_x8_yw8rV7Q_QRBYayqtTNsPdZvjzOxWpRhXP1XAzs,3119
42
43
  argus/backend/plugins/sirenada/model.py,sha256=KVnI75BacuBryc5lR_Aai-mEOs7CB9xxhb7J-YRU3bc,4705
43
44
  argus/backend/plugins/sirenada/plugin.py,sha256=AlQAakwy3u-OqAqqK3RonUR5oDm-JoiwBUDUF3YEVP4,447
44
45
  argus/backend/plugins/sirenada/types.py,sha256=Gm3XMK9YJoozVaeM9XE7n8iRxA6PKBrS23Mo2vJfdLs,697
45
46
  argus/backend/service/admin.py,sha256=_VnWl3CkZBOAie_pPbd9sbXZUpBf2SApyNoFZLfB_QI,637
46
- argus/backend/service/argus_service.py,sha256=d1NrCQmcE_1NmkV761dheBL43Vk0n-oP0S4IZtQzJUQ,28832
47
- argus/backend/service/build_system_monitor.py,sha256=E8Ro1HBMfktxqcCHLPuXwzohkdgrls7pYro-VSG_CMs,8248
48
- argus/backend/service/client_service.py,sha256=CS5esppd9s-SgUYE-HVLkfz-MrN8zxPouf9e4VlPV_M,2326
47
+ argus/backend/service/argus_service.py,sha256=YF6El9CyIelePDrCydVn4K82sd7CzCoZNmcvn2ZeR9I,29266
48
+ argus/backend/service/build_system_monitor.py,sha256=QB7RfMMuA2VJ4oUAOAqLxOwxqaQE52_4ZhsASVcoXkU,8296
49
+ argus/backend/service/client_service.py,sha256=wcL_Yv_6Q5P4JnvrBMFA_SGVwZqTdK0tjj8q_yt_MAk,3726
49
50
  argus/backend/service/event_service.py,sha256=iYeqxN2QCYTjYB1WPPv4BEFLXG0Oz3TvskkaK4v9pVY,654
50
- argus/backend/service/jenkins_service.py,sha256=PKmu44kAEWl-Dw7f8gXkKhwRkV4yWy-B4CmFxv7c6-8,9685
51
+ argus/backend/service/jenkins_service.py,sha256=njomagkliIWKisR9FmhKKqZ8y9NijyJ3hUQe23gl2U4,9878
51
52
  argus/backend/service/notification_manager.py,sha256=h00Ej_-hH9H7pq0wah_1TH8dnpPyPNsgVJNO1rwJi7o,7011
52
53
  argus/backend/service/release_manager.py,sha256=d1J6llBb4aKgFPrsPTPYpV9NnGx772jeORZjs-ojYGE,7771
54
+ argus/backend/service/results_service.py,sha256=t2IW05avpl1bl8TJDjGo_bt0gswwoM-4vOnnekGCVAc,4528
53
55
  argus/backend/service/stats.py,sha256=-V94A8EUlQBvwG53oJTL4U1EzR4vciEF7Niu-efTL6Y,22713
54
56
  argus/backend/service/team_manager_service.py,sha256=zY5dvy3ffvQbJuXBvlWKE5dS5LQ3ss6tkFE-cwFZsdw,3010
55
- argus/backend/service/testrun.py,sha256=ZM0D38IQpbrmia1GlCfLK02ZLwp_pbpUuQPN8BwibAw,21432
56
- argus/backend/service/user.py,sha256=N3t43rgKMnSsPXU5R9bigEEGbPjYrc6MsJtof3z7kDE,9027
57
- argus/backend/service/views.py,sha256=amkyOQgHSP3YEDqiFHAhOiNzNCLk2sLkruTB2_IagDQ,11244
57
+ argus/backend/service/testrun.py,sha256=fPME2Eq02rKobL_QxFwR2jXn5bG_RSS4EN1UVMzRRPY,22735
58
+ argus/backend/service/user.py,sha256=DC8fII7mElWGB-pMGyn4uzaJHIbmxzh_ZWf2POmlBkU,10936
59
+ argus/backend/service/views.py,sha256=gUzwQv3fasGh0hRvivCr64XooQhG3c1V1KcxgMjC2qM,11292
58
60
  argus/backend/template_filters.py,sha256=04PHl0DiN4PBHQ82HMAmTfww09fGMXcYy-I5BU_b1s4,682
59
61
  argus/backend/util/common.py,sha256=vLMit9ZBBN8S4-dw32LIhjtaEOX_5hwWneHILS_SNBg,1723
60
- argus/backend/util/config.py,sha256=Sm0LCRkabYaSUkXNPglyjMr45GCDBNXqJLkmB_s51f0,860
61
- argus/backend/util/encoders.py,sha256=VxOnUanHP9FjcaobYupV-pZ3Udzrrrq7zZcbNpVXaKM,646
62
+ argus/backend/util/config.py,sha256=1HpHm8Du6yz61gwAE1vR6uwuHCStaSerirbEhBLnDws,927
63
+ argus/backend/util/encoders.py,sha256=5AfJbs2V3gOOg5LtFLZAtBqlnSdX8HHITT7r9Wu-law,1129
62
64
  argus/backend/util/enums.py,sha256=EhTQrgedlEz5sDYJ1gFnE2eC2nc1neQCRgzOgssQvWY,749
63
65
  argus/backend/util/logsetup.py,sha256=XWyFLC1_J5G8NcR7oC9l-Pf02ybAvEZR95y5LoY4W48,2438
64
66
  argus/backend/util/module_loaders.py,sha256=AcIlX-VRmUQ2THFKT8DLefLSE62Eub2hCxIosg3WgE0,698
65
67
  argus/backend/util/send_email.py,sha256=Bb2Hta7WlBCvDKga0_WPFWgxWJEfKpectOGypgf9xzo,3217
66
68
  argus/client/__init__.py,sha256=bO9_j5_jK5kvTHR46KEZ0Y-p0li7CBW8QSd-K5Ez4vA,42
67
- argus/client/base.py,sha256=-R-BINTolY06lUQLOLGlsWzza4fBdtLBW-4V3NT64vg,6755
69
+ argus/client/base.py,sha256=ihSNHlcpChYafooUHnzFI8aYqIkk4fD_qVF4iYIDbQ8,7994
68
70
  argus/client/driver_matrix_tests/cli.py,sha256=PIK4IyA4qku7jCnJ8A0i59DeVl1jvMWYukeMOqUQ0jM,6346
69
71
  argus/client/driver_matrix_tests/client.py,sha256=UPryBku2rg6IV2wKKDkclXHnH3r6EYwWdds65wLC-KU,2748
70
72
  argus/client/generic/cli.py,sha256=IJkgEZ5VOAeqp5SlLM13Y5m8e34Cqnyz8WkfeKoN7so,2208
71
73
  argus/client/generic/client.py,sha256=l4PDjDy65Mm2OI9ZLSnyd8_2i4Ei1Pp9yRt3bRX8s2Y,1114
74
+ argus/client/generic_result.py,sha256=Xg3MwwulHH7dGk_pwwbVOm4cHzV4qbfHaFD0VteFVHI,2666
72
75
  argus/client/sct/client.py,sha256=DtRA0Ra3ycUcedDYfZZW1jER0nc8vdYHaY6DT0te4x0,11341
73
76
  argus/client/sct/types.py,sha256=VLgVe7qPmJtCLqtPnuX8N8kMKZq-iY3SKz68nvU6nJ4,371
74
77
  argus/client/sirenada/client.py,sha256=ilcyLXJb-0gKbmb9WSPr-Yvldh73joGBhRDoilQoSJ4,6220
@@ -80,8 +83,8 @@ argus/db/db_types.py,sha256=iLbmrUaDzrBw0kDCnvW0FSZ9-kNc3uQY-fsbIPymV4E,3612
80
83
  argus/db/interface.py,sha256=HroyA1Yijz5cXLdYbxorHCEu0GH9VeMMqB36IHTlcew,17146
81
84
  argus/db/testrun.py,sha256=0YG7FIH5FLQeNlYULxC6rhhyru2rziSMe3qKtYzTBnc,26014
82
85
  argus/db/utils.py,sha256=YAWsuLjUScSgKgdaL5aF4Sgr13gqH29Mb5cLctX4V_w,337
83
- argus_alm-0.12.4b2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
84
- argus_alm-0.12.4b2.dist-info/METADATA,sha256=bF-azdmr9Fwi30Aj_jt1znGiMdXenmDqAUMC0TeV1hA,3510
85
- argus_alm-0.12.4b2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
86
- argus_alm-0.12.4b2.dist-info/entry_points.txt,sha256=pcYW8nxZuDaymxE8tn86K0dq8eEodUdiS0sSvwEQ_zU,137
87
- argus_alm-0.12.4b2.dist-info/RECORD,,
86
+ argus_alm-0.12.7.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
87
+ argus_alm-0.12.7.dist-info/METADATA,sha256=5mRLCu-5qnJwLHJmwc3yBlZaBqr_ihdnXb2vo_yiqjs,3508
88
+ argus_alm-0.12.7.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
89
+ argus_alm-0.12.7.dist-info/entry_points.txt,sha256=pcYW8nxZuDaymxE8tn86K0dq8eEodUdiS0sSvwEQ_zU,137
90
+ argus_alm-0.12.7.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.8.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any