deltafi 2.0rc1719271450675__py3-none-any.whl → 2.0rc1720728217472__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.

Potentially problematic release.


This version of deltafi might be problematic. Click here for more details.

deltafi/result.py CHANGED
@@ -17,9 +17,9 @@
17
17
  #
18
18
 
19
19
  import abc
20
- import uuid
21
20
  from enum import Enum
22
- from typing import NamedTuple
21
+ import uuid
22
+ from typing import Dict, List
23
23
 
24
24
  from deltafi.domain import Content, Context
25
25
  from deltafi.metric import Metric
@@ -44,8 +44,22 @@ class Result:
44
44
 
45
45
  def add_metric(self, metric: Metric):
46
46
  self.metrics.append(metric)
47
+
48
+
49
+ class DomainResult(Result):
50
+ def __init__(self, context: Context):
51
+ super().__init__('domain', 'DOMAIN', context)
52
+ self.annotations = {}
53
+
54
+ def annotate(self, key: str, value: str):
55
+ self.annotations[key] = value
47
56
  return self
48
57
 
58
+ def response(self):
59
+ return {
60
+ 'annotations': self.annotations
61
+ }
62
+
49
63
 
50
64
  class EgressResult(Result):
51
65
  def __init__(self, context: Context, destination: str, bytes_egressed: int):
@@ -57,6 +71,31 @@ class EgressResult(Result):
57
71
  return None
58
72
 
59
73
 
74
+ class EnrichResult(Result):
75
+ def __init__(self, context: Context):
76
+ super().__init__('enrich', 'ENRICH', context)
77
+ self.enrichments = []
78
+ self.annotations = {}
79
+
80
+ def enrich(self, name: str, value: str, media_type: str):
81
+ self.enrichments.append({
82
+ 'name': name,
83
+ 'value': value,
84
+ 'mediaType': media_type
85
+ })
86
+ return self
87
+
88
+ def annotate(self, key: str, value: str):
89
+ self.annotations[key] = value
90
+ return self
91
+
92
+ def response(self):
93
+ return {
94
+ 'enrichments': self.enrichments,
95
+ 'annotations': self.annotations
96
+ }
97
+
98
+
60
99
  class ErrorResult(Result):
61
100
  def __init__(self, context: Context, error_cause: str, error_context: str):
62
101
  super().__init__('error', 'ERROR', context)
@@ -77,7 +116,7 @@ class ErrorResult(Result):
77
116
 
78
117
 
79
118
  class FilterResult(Result):
80
- def __init__(self, context: Context, filtered_cause: str, filtered_context: str = None):
119
+ def __init__(self, context: Context, filtered_cause: str, filtered_context: str=None):
81
120
  super().__init__('filter', 'FILTER', context)
82
121
  self.filtered_cause = filtered_cause
83
122
  self.filtered_context = filtered_context
@@ -95,13 +134,87 @@ class FilterResult(Result):
95
134
  }
96
135
 
97
136
 
137
+ class FormatResult(Result):
138
+ def __init__(self, context: Context):
139
+ super().__init__('format', 'FORMAT', context)
140
+ self.content = None
141
+ self.delete_metadata_keys = []
142
+ self.metadata = {}
143
+
144
+ def set_metadata(self, metadata: dict):
145
+ self.metadata = metadata
146
+ return self
147
+
148
+ def add_metadata(self, key: str, value: str):
149
+ self.metadata[key] = value
150
+ return self
151
+
152
+ def delete_metadata_key(self, key: str):
153
+ self.delete_metadata_keys.append(key)
154
+ return self
155
+
156
+ def set_content(self, content: Content):
157
+ self.content = content
158
+ return self
159
+
160
+ def save_string_content(self, string_data: str, name: str, media_type: str):
161
+ segment = self.context.content_service.put_str(self.context.did, string_data)
162
+ self.content = Content(name=name, segments=[segment], media_type=media_type,
163
+ content_service=self.context.content_service)
164
+ return self
165
+
166
+ def save_byte_content(self, byte_data: bytes, name: str, media_type: str):
167
+ segment = self.context.content_service.put_bytes(self.context.did, byte_data)
168
+ self.content = Content(name=name, segments=[segment], media_type=media_type,
169
+ content_service=self.context.content_service)
170
+ return self
171
+
172
+ def response(self):
173
+ return {
174
+ 'content': self.content.json(),
175
+ 'metadata': self.metadata,
176
+ 'deleteMetadataKeys': self.delete_metadata_keys
177
+ }
178
+
179
+
180
+ class ChildFormatResult:
181
+ def __init__(self, format_result: FormatResult = None):
182
+ self._did = str(uuid.uuid4())
183
+ self.format_result = format_result
184
+
185
+ @property
186
+ def did(self):
187
+ return self._did
188
+
189
+ def response(self):
190
+ res = self.format_result.response()
191
+ res["did"] = self._did
192
+ return res
193
+
194
+
195
+ class FormatManyResult(Result):
196
+ def __init__(self, context: Context):
197
+ super().__init__('formatMany', 'FORMAT_MANY', context)
198
+ self.format_results = []
199
+
200
+ def add_format_result(self, format_result):
201
+ if isinstance(format_result, ChildFormatResult):
202
+ self.format_results.append(format_result)
203
+ else:
204
+ self.format_results.append(ChildFormatResult(format_result))
205
+ return self
206
+
207
+ def response(self):
208
+ return [format_result.response() for format_result in self.format_results]
209
+
210
+
98
211
  class IngressResultItem:
99
- def __init__(self, context: Context, delta_file_name: str):
212
+ def __init__(self, context: Context, filename: str):
100
213
  self.context = context
214
+ self.filename = filename
101
215
  self._did = str(uuid.uuid4())
102
216
  self.content = []
103
217
  self.metadata = {}
104
- self.delta_file_name = delta_file_name
105
218
 
106
219
  @property
107
220
  def did(self):
@@ -140,7 +253,7 @@ class IngressResultItem:
140
253
  def response(self):
141
254
  return {
142
255
  'did': self._did,
143
- 'deltaFileName': self.delta_file_name,
256
+ 'filename': self.filename,
144
257
  'metadata': self.metadata,
145
258
  'content': [content.json() for content in self.content]
146
259
  }
@@ -156,10 +269,10 @@ class IngressResult(Result):
156
269
  def __init__(self, context: Context):
157
270
  super().__init__('ingress', 'INGRESS', context)
158
271
  self.memo = None
159
- self.ingress_result_items = []
160
272
  self.execute_immediate = False
273
+ self.ingress_result_items = []
161
274
  self.status = IngressStatusEnum.HEALTHY
162
- self.status_message = None
275
+ self.statusMessage = None
163
276
 
164
277
  def add_item(self, ingress_result_item: IngressResultItem):
165
278
  self.ingress_result_items.append(ingress_result_item)
@@ -171,16 +284,17 @@ class IngressResult(Result):
171
284
  'executeImmediate': self.execute_immediate,
172
285
  'ingressItems': [ingress_result_item.response() for ingress_result_item in self.ingress_result_items],
173
286
  'status': self.status.value,
174
- 'statusMessage': self.status_message
287
+ 'statusMessage': self.statusMessage
175
288
  }
176
289
 
177
290
 
178
- class TransformResult(Result):
291
+ class LoadResult(Result):
179
292
  def __init__(self, context: Context):
180
- super().__init__('transform', 'TRANSFORM', context)
293
+ super().__init__('load', 'LOAD', context)
181
294
  self.content = []
182
- self.annotations = {}
183
295
  self.metadata = {}
296
+ self.domains = []
297
+ self.annotations = {}
184
298
  self.delete_metadata_keys = []
185
299
 
186
300
  # content can be a single Content or a List[Content]
@@ -213,6 +327,13 @@ class TransformResult(Result):
213
327
  self.metadata[key] = value
214
328
  return self
215
329
 
330
+ def add_domain(self, name: str, value: str, media_type: str):
331
+ self.domains.append({
332
+ 'name': name,
333
+ 'value': value,
334
+ 'mediaType': media_type})
335
+ return self
336
+
216
337
  def annotate(self, key: str, value: str):
217
338
  self.annotations[key] = value
218
339
  return self
@@ -221,41 +342,133 @@ class TransformResult(Result):
221
342
  self.delete_metadata_keys.append(key)
222
343
  return self
223
344
 
224
- def json(self):
345
+ def response(self):
225
346
  return {
347
+ 'domains': self.domains,
226
348
  'content': [content.json() for content in self.content],
227
- 'annotations': self.annotations,
228
349
  'metadata': self.metadata,
350
+ 'annotations': self.annotations,
229
351
  'deleteMetadataKeys': self.delete_metadata_keys
230
352
  }
231
353
 
354
+
355
+ class ChildLoadResult:
356
+ def __init__(self, load_result: LoadResult = None):
357
+ self._did = str(uuid.uuid4())
358
+ self.load_result = load_result
359
+
360
+ @property
361
+ def did(self):
362
+ return self._did
363
+
364
+ def response(self):
365
+ res = self.load_result.response()
366
+ res["did"] = self._did
367
+ return res
368
+
369
+
370
+ class LoadManyResult(Result):
371
+ def __init__(self, context: Context):
372
+ super().__init__('loadMany', 'LOAD_MANY', context)
373
+ self.load_results = []
374
+
375
+ def add_load_result(self, load_result):
376
+ if isinstance(load_result, ChildLoadResult):
377
+ self.load_results.append(load_result)
378
+ else:
379
+ self.load_results.append(ChildLoadResult(load_result))
380
+ return self
381
+
232
382
  def response(self):
233
- return [self.json()]
383
+ return [load_result.response() for load_result in self.load_results]
384
+
385
+
386
+ class ReinjectResult(Result):
387
+ class ReinjectChild:
388
+ def __init__(self, filename: str, flow: str, content: List[Content], metadata: Dict[str, str]):
389
+ self.filename = filename
390
+ self.flow = flow
391
+ self.content = content
392
+ self.metadata = metadata
234
393
 
394
+ def json(self):
395
+ return {
396
+ 'filename': self.filename,
397
+ 'flow': self.flow,
398
+ 'metadata': self.metadata,
399
+ 'content': [content.json() for content in self.content]
400
+ }
401
+
402
+ def __init__(self, context: Context):
403
+ super().__init__('reinject', 'REINJECT', context)
404
+ self.children = []
235
405
 
236
- class NamedTransformResult(NamedTuple):
237
- result: TransformResult
238
- name: str
406
+ def add_child(self, filename: str, flow: str, content: List[Content], metadata: Dict[str, str]):
407
+ child = ReinjectResult.ReinjectChild(filename, flow, content, metadata)
408
+ self.children.append(child)
239
409
 
240
- def json(self):
241
- j = self.result.json()
242
- if self.name is not None:
243
- j['name'] = self.name
244
- return j
410
+ def response(self):
411
+ return [child.json() for child in self.children]
245
412
 
246
413
 
247
- class TransformResults(Result):
414
+ class TransformResult(Result):
248
415
  def __init__(self, context: Context):
249
416
  super().__init__('transform', 'TRANSFORM', context)
250
- self.named_results = []
417
+ self.content = []
418
+ self.metadata = {}
419
+ self.annotations = {}
420
+ self.delete_metadata_keys = []
421
+
422
+ # content can be a single Content or a List[Content]
423
+ def add_content(self, content):
424
+ if content:
425
+ if type(content) == list:
426
+ self.content.extend(content)
427
+ else:
428
+ self.content.append(content)
251
429
 
252
- def add_result(self, result: TransformResult, name: str = None):
253
- self.named_results.append(NamedTransformResult(result, name))
430
+ return self
431
+
432
+ def save_string_content(self, string_data: str, name: str, media_type: str):
433
+ segment = self.context.content_service.put_str(self.context.did, string_data)
434
+ self.content.append(
435
+ Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
436
+ return self
437
+
438
+ def save_byte_content(self, byte_data: bytes, name: str, media_type: str):
439
+ segment = self.context.content_service.put_bytes(self.context.did, byte_data)
440
+ self.content.append(
441
+ Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
442
+ return self
443
+
444
+ def set_metadata(self, metadata: dict):
445
+ self.metadata = metadata
446
+ return self
447
+
448
+ def add_metadata(self, key: str, value: str):
449
+ self.metadata[key] = value
450
+ return self
451
+
452
+ def annotate(self, key: str, value: str):
453
+ self.annotations[key] = value
454
+ return self
455
+
456
+ def delete_metadata_key(self, key: str):
457
+ self.delete_metadata_keys.append(key)
254
458
  return self
255
459
 
256
460
  def response(self):
257
- transform_events = []
258
- for named_result in self.named_results:
259
- json_dict = named_result.json()
260
- transform_events.append(json_dict)
261
- return transform_events
461
+ return {
462
+ 'content': [content.json() for content in self.content],
463
+ 'metadata': self.metadata,
464
+ 'annotations': self.annotations,
465
+ 'deleteMetadataKeys': self.delete_metadata_keys
466
+ }
467
+
468
+
469
+ class ValidateResult(Result):
470
+ def __init__(self, context: Context):
471
+ super().__init__(None, 'VALIDATE', context)
472
+
473
+ def response(self):
474
+ return None
@@ -0,0 +1,105 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ from typing import List
20
+
21
+ from deltafi.result import FormatResult, FormatManyResult
22
+
23
+ from .assertions import *
24
+ from .framework import TestCaseBase, ActionTest
25
+
26
+
27
+ class FormatTestCase(TestCaseBase):
28
+ def __init__(self, fields: Dict):
29
+ super().__init__(fields)
30
+ self.metadata = {}
31
+ self.delete_metadata_keys = []
32
+ self.expected_format_many_result = []
33
+
34
+ def expect_format_result(self, metadata: Dict, delete_metadata_keys: List[str]):
35
+ self.expected_result_type = FormatResult
36
+ self.metadata = metadata
37
+ self.delete_metadata_keys = delete_metadata_keys
38
+
39
+ def add_format_many_result(self, metadata: Dict, delete_metadata_keys: List):
40
+ self.expected_result_type = FormatManyResult
41
+ self.expected_format_many_result.append(
42
+ {
43
+ "metadata": metadata,
44
+ "delete_metadata_keys": delete_metadata_keys
45
+ }
46
+ )
47
+
48
+
49
+ class FormatActionTest(ActionTest):
50
+ def __init__(self, package_name: str):
51
+ """
52
+ Provides structure for testing DeltaFi Format action
53
+ Args:
54
+ package_name: name of the actions package for finding resources
55
+ """
56
+ super().__init__(package_name)
57
+
58
+ def format(self, test_case: FormatTestCase):
59
+ if test_case.expected_result_type == FormatManyResult:
60
+ self.expect_format_many_result(test_case)
61
+ elif test_case.expected_result_type == FormatResult:
62
+ self.expect_format_result(test_case)
63
+ else:
64
+ super().execute(test_case)
65
+
66
+ def expect_format_result(self, test_case: FormatTestCase):
67
+ result = super().run_and_check_result_type(test_case, FormatResult)
68
+ self.assert_format_result(test_case, result)
69
+
70
+ def expect_format_many_result(self, test_case: FormatTestCase):
71
+ result = super().run_and_check_result_type(test_case, FormatManyResult)
72
+ self.assert_format_many_result(test_case, result)
73
+
74
+ def assert_format_result(self, test_case: FormatTestCase, result: FormatResult):
75
+ # Check metrics
76
+ self.compare_metrics(test_case.expected_metrics, result.metrics)
77
+
78
+ # Check output
79
+ if result.content is None:
80
+ self.compare_all_output(test_case.compare_tool, [])
81
+ else:
82
+ self.compare_all_output(test_case.compare_tool, [result.content])
83
+
84
+ # Check metadata
85
+ assert_keys_and_values(test_case.metadata, result.metadata)
86
+
87
+ # Check deleted metadata
88
+ for key in test_case.delete_metadata_keys:
89
+ assert_key_in(key, result.delete_metadata_keys)
90
+
91
+ def assert_format_many_result(self, test_case: FormatTestCase, actual: FormatManyResult):
92
+ # Check metrics
93
+ self.compare_metrics(test_case.expected_metrics, actual.metrics)
94
+
95
+ assert_equal_len(test_case.expected_format_many_result, actual.format_results)
96
+ for index, expected_child_result in enumerate(test_case.expected_format_many_result):
97
+ actual_child = actual.format_results[index]
98
+ self.compare_one_content(
99
+ test_case.compare_tool,
100
+ self.expected_outputs[index],
101
+ actual_child.format_result.content, index)
102
+
103
+ assert_keys_and_values(expected_child_result['metadata'], actual_child.format_result.metadata)
104
+ for key in expected_child_result['delete_metadata_keys']:
105
+ assert_key_in(key, actual_child.format_result.delete_metadata_keys)
@@ -103,10 +103,6 @@ class InternalContentService:
103
103
  seg_id = segments[0].uuid
104
104
  return self.loaded_content[seg_id].data
105
105
 
106
- def get_bytes(self, segments: List[Segment]):
107
- seg_id = segments[0].uuid
108
- return self.loaded_content[seg_id].data.encode('utf-8')
109
-
110
106
  def get_output(self, seg_id: str):
111
107
  if seg_id in self.outputs:
112
108
  return self.outputs[seg_id]
@@ -127,6 +123,8 @@ class TestCaseBase(ABC):
127
123
  - inputs: (optional) List[IOContent]: input content to action
128
124
  - parameters: (optional) Dict: map of action input parameters
129
125
  - in_meta: (optional) Dict: map of metadata as input to action
126
+ - in_domains: (optional) List[Domain]: list of domains as input to action
127
+ - in_enrichments: (optional) List[Domain]: list of enrichments as input to action
130
128
  - did: (optional): str: overrides random DID
131
129
  """
132
130
  if "action" in data:
@@ -146,8 +144,11 @@ class TestCaseBase(ABC):
146
144
 
147
145
  self.inputs = data["inputs"] if "inputs" in data else []
148
146
  self.file_name = data["file_name"] if "file_name" in data else "filename"
147
+ self.outputs = data["outputs"] if "outputs" in data else []
149
148
  self.parameters = data["parameters"] if "parameters" in data else {}
150
149
  self.in_meta = data["in_meta"] if "in_meta" in data else {}
150
+ self.in_domains = data["in_domains"] if "in_domains" in data else []
151
+ self.in_enrichments = data["in_enrichments"] if "in_enrichments" in data else []
151
152
  self.use_did = data["did"] if "did" in data else None
152
153
  self.expected_result_type = None
153
154
  self.err_or_filt_cause = None
@@ -192,6 +193,7 @@ class ActionTest(ABC):
192
193
  """
193
194
  self.content_service = InternalContentService()
194
195
  self.did = ""
196
+ self.expected_outputs = []
195
197
  self.loaded_inputs = []
196
198
  self.package_name = package_name
197
199
  self.res_path = ""
@@ -202,6 +204,7 @@ class ActionTest(ABC):
202
204
  self.did = str(uuid.uuid4())
203
205
  else:
204
206
  self.did = did
207
+ self.expected_outputs = []
205
208
  self.loaded_inputs = []
206
209
  self.res_path = ""
207
210
 
@@ -222,6 +225,13 @@ class ActionTest(ABC):
222
225
  else:
223
226
  self.loaded_inputs.append(LoadedContent(self.did, input_ioc, None))
224
227
 
228
+ # Load expected outputs
229
+ for output_ioc in test_case.outputs:
230
+ if len(output_ioc.content_bytes) == 0:
231
+ self.expected_outputs.append(LoadedContent(self.did, output_ioc, self.load_file(output_ioc)))
232
+ else:
233
+ self.expected_outputs.append(LoadedContent(self.did, output_ioc, None))
234
+
225
235
  def make_content_list(self, test_case: TestCaseBase):
226
236
  content_list = []
227
237
  for loaded_input in self.loaded_inputs:
@@ -236,23 +246,26 @@ class ActionTest(ABC):
236
246
  content_list = self.make_content_list(test_case)
237
247
  self.content_service.load(self.loaded_inputs)
238
248
 
239
- return DeltaFileMessage(metadata=test_case.in_meta,
240
- content_list=content_list)
249
+ return DeltaFileMessage(
250
+ metadata=test_case.in_meta,
251
+ content_list=content_list,
252
+ domains=test_case.in_domains,
253
+ enrichments=test_case.in_enrichments)
241
254
 
242
255
  def make_context(self, test_case: TestCaseBase):
243
256
  action_name = INGRESS_FLOW + "." + test_case.action.__class__.__name__
244
257
  return Context(
245
258
  did=self.did,
246
- delta_file_name=test_case.file_name,
247
- data_source="DATASRC",
248
- flow_name=INGRESS_FLOW,
249
- flow_id="FLOWID",
259
+ action_flow=INGRESS_FLOW,
250
260
  action_name=action_name,
251
- action_id="ACTIONID",
252
- action_version="1.0",
261
+ source_filename=test_case.file_name,
262
+ ingress_flow=INGRESS_FLOW,
263
+ egress_flow=EGRESS_FLOW,
264
+ system=SYSTEM,
253
265
  hostname=HOSTNAME,
254
- system_name=SYSTEM,
255
266
  content_service=self.content_service,
267
+ collect=None,
268
+ collected_dids=None,
256
269
  logger=get_logger())
257
270
 
258
271
  def make_event(self, test_case: TestCaseBase):
@@ -298,27 +311,37 @@ class ActionTest(ABC):
298
311
  else:
299
312
  raise ValueError(f"unknown type: {test_case.expected_result_type}")
300
313
 
301
- @staticmethod
302
- def compare_content_details(expected: LoadedContent, actual: Content):
314
+ def compare_content_details(self, expected: LoadedContent, actual: Content):
303
315
  assert_equal(expected.content_type, actual.media_type)
304
316
  assert_equal(expected.name, actual.name)
305
317
 
306
- def compare_one_content(self, comparator: CompareHelper, expected: LoadedContent, actual, index):
318
+ def compare_one_content(self, comparitor: CompareHelper, expected: LoadedContent, actual, index):
307
319
  self.compare_content_details(expected, actual)
308
320
  seg_id = actual.segments[0].uuid
309
- comparator.compare(expected.data, self.content_service.get_output(seg_id), f"Content[{index}]")
310
-
311
- def compare_content_list(self, comparator: CompareHelper, expected_outputs: List[IOContent], content: List):
312
- assert_equal_len(expected_outputs, content)
313
- for index, expected_ioc in enumerate(expected_outputs):
314
- if len(expected_ioc.content_bytes) == 0:
315
- expected = LoadedContent(self.did, expected_ioc, self.load_file(expected_ioc))
321
+ comparitor.compare(expected.data, self.content_service.get_output(seg_id), f"Content[{index}]")
322
+
323
+ def compare_all_output(self, comparitor: CompareHelper, content: List):
324
+ assert_equal_len(self.expected_outputs, content)
325
+ for index, expected in enumerate(self.expected_outputs):
326
+ self.compare_one_content(comparitor, expected, content[index], index)
327
+
328
+ def compare_domains(self, comparitor: CompareHelper, expected_items: List[Dict], results: List[Dict]):
329
+ assert_equal_len(expected_items, results)
330
+ for index, expected in enumerate(expected_items):
331
+ actual = results[index]
332
+ assert_equal(expected['name'], actual['name'])
333
+ assert_equal(expected['mediaType'], actual['mediaType'])
334
+
335
+ expected_value = expected['value']
336
+ if type(expected_value) == str:
337
+ comparitor.compare(expected_value, actual['value'], f"Domain[{index}]")
338
+ elif type(expected_value) == IOContent:
339
+ expected_data = self.load_file(expected_value)
340
+ comparitor.compare(expected_data, actual['value'], f"Domain[{index}]")
316
341
  else:
317
- expected = LoadedContent(self.did, expected_ioc, None)
318
- self.compare_one_content(comparator, expected, content[index], index)
342
+ raise ValueError(f"unknown expected_value type: {type(expected_value)}")
319
343
 
320
- @staticmethod
321
- def compare_one_metric(expected: Metric, result: Metric):
344
+ def compare_one_metric(self, expected: Metric, result: Metric):
322
345
  assert expected.name == result.name
323
346
  assert_equal_with_label(expected.value, result.value, expected.name)
324
347
  assert_keys_and_values(expected.tags, result.tags)