deltafi 2.0rc1705024454242__tar.gz → 2.4.0__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.

Potentially problematic release.


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

Files changed (27) hide show
  1. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/PKG-INFO +13 -12
  2. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/__init__.py +1 -1
  3. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/action.py +36 -20
  4. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/actioneventqueue.py +1 -1
  5. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/actiontype.py +3 -1
  6. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/domain.py +73 -63
  7. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/exception.py +1 -11
  8. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/genericmodel.py +4 -2
  9. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/input.py +1 -1
  10. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/logger.py +4 -4
  11. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/metric.py +2 -2
  12. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/plugin.py +198 -50
  13. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/result.py +95 -31
  14. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/storage.py +6 -1
  15. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/test_kit/__init__.py +1 -1
  16. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/test_kit/assertions.py +10 -2
  17. deltafi-2.4.0/deltafi/test_kit/compare_helpers.py +293 -0
  18. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/deltafi/test_kit/constants.py +1 -1
  19. deltafi-2.4.0/deltafi/test_kit/egress.py +54 -0
  20. deltafi-2.4.0/deltafi/test_kit/framework.py +365 -0
  21. deltafi-2.4.0/deltafi/test_kit/timed_ingress.py +101 -0
  22. deltafi-2.4.0/deltafi/test_kit/transform.py +102 -0
  23. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/pyproject.toml +16 -15
  24. deltafi-2.0rc1705024454242/deltafi/test_kit/compare_helpers.py +0 -50
  25. deltafi-2.0rc1705024454242/deltafi/test_kit/framework.py +0 -331
  26. deltafi-2.0rc1705024454242/deltafi/test_kit/transform.py +0 -72
  27. {deltafi-2.0rc1705024454242 → deltafi-2.4.0}/README.md +0 -0
@@ -1,31 +1,32 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: deltafi
3
- Version: 2.0rc1705024454242
3
+ Version: 2.4.0
4
4
  Summary: SDK for DeltaFi plugins and actions
5
5
  License: Apache License, Version 2.0
6
6
  Keywords: deltafi
7
7
  Author: DeltaFi
8
8
  Author-email: deltafi@systolic.com
9
- Requires-Python: >=3.7,<4.0
9
+ Requires-Python: >=3.9,<4.0
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: License :: Other/Proprietary License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
20
18
  Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development
22
- Requires-Dist: deepdiff (>=6.3.1)
23
- Requires-Dist: json-logging (>=1.3.0)
24
- Requires-Dist: minio (>=7.1.17)
25
- Requires-Dist: pydantic (>=2.4.2)
26
- Requires-Dist: redis (>=5.0.1)
27
- Requires-Dist: requests (>=2.31.0)
28
- Requires-Dist: urllib3 (>=2.0.6)
22
+ Requires-Dist: PyYAML (==6.0.2)
23
+ Requires-Dist: deepdiff (==6.7.1)
24
+ Requires-Dist: json-logging (==1.3.0)
25
+ Requires-Dist: minio (==7.2.10)
26
+ Requires-Dist: pydantic (==2.9.2)
27
+ Requires-Dist: redis (==5.2.0)
28
+ Requires-Dist: requests (==2.32.3)
29
+ Requires-Dist: urllib3 (==2.2.3)
29
30
  Project-URL: Bug Reports, https://chat.deltafi.org/deltafi/channels/bug-reports
30
31
  Project-URL: Documentation, https://docs.deltafi.org/#/
31
32
  Project-URL: Source Code, https://gitlab.com/deltafi/deltafi
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -20,13 +20,23 @@ from abc import ABC, abstractmethod
20
20
  from typing import Any, List
21
21
 
22
22
  from deltafi.actiontype import ActionType
23
- from deltafi.genericmodel import GenericModel
24
23
  from deltafi.domain import Context, DeltaFileMessage
24
+ from deltafi.genericmodel import GenericModel
25
25
  from deltafi.input import EgressInput, TransformInput
26
26
  from deltafi.result import *
27
27
  from pydantic import BaseModel
28
28
 
29
29
 
30
+ class Join(ABC):
31
+ def join(self, transform_inputs: List[TransformInput]):
32
+ all_content = []
33
+ all_metadata = {}
34
+ for transform_input in transform_inputs:
35
+ all_content += transform_input.content
36
+ all_metadata.update(transform_input.metadata)
37
+ return TransformInput(content=all_content, metadata=all_metadata)
38
+
39
+
30
40
  class Action(ABC):
31
41
  def __init__(self, action_type: ActionType, description: str, valid_result_types: tuple):
32
42
  self.action_type = action_type
@@ -38,8 +48,8 @@ class Action(ABC):
38
48
  def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
39
49
  pass
40
50
 
41
- def collect(self, action_inputs: List[Any]):
42
- raise RuntimeError(f"Collect is not supported for {self.__class__.__name__}")
51
+ def execute_join_action(self, event):
52
+ raise RuntimeError(f"Join is not supported for {self.__class__.__name__}")
43
53
 
44
54
  @abstractmethod
45
55
  def execute(self, context: Context, action_input: Any, params: BaseModel):
@@ -48,22 +58,25 @@ class Action(ABC):
48
58
  def execute_action(self, event):
49
59
  if event.delta_file_messages is None or not len(event.delta_file_messages):
50
60
  raise RuntimeError(f"Received event with no delta file messages for did {event.context.did}")
51
-
52
- if event.context.collect is not None:
53
- result = self.execute(event.context, self.collect([self.build_input(event.context, delta_file_message)
54
- for delta_file_message in event.delta_file_messages]),
55
- self.param_class().model_validate(event.params))
61
+ if event.context.join is not None:
62
+ result = self.execute_join_action(event)
56
63
  else:
57
- result = self.execute(event.context, self.build_input(event.context, event.delta_file_messages[0]),
58
- self.param_class().model_validate(event.params))
64
+ result = self.execute(
65
+ event.context,
66
+ self.build_input(event.context, event.delta_file_messages[0]),
67
+ self.param_class().model_validate(event.params))
59
68
 
60
69
  self.validate_type(result)
61
70
  return result
62
71
 
63
72
  @staticmethod
64
- def param_class( ):
73
+ def param_class():
65
74
  """Factory method to create and return an empty GenericModel instance.
66
75
 
76
+ All action parameter classes must inherit pydantic.BaseModel.
77
+ Use of complex types in custom action parameter classes must specify
78
+ the internal types when defined. E.g., dict[str, str], or List[str]
79
+
67
80
  Returns
68
81
  -------
69
82
  GenericModel
@@ -110,18 +123,21 @@ class TimedIngressAction(Action, ABC):
110
123
 
111
124
  class TransformAction(Action, ABC):
112
125
  def __init__(self, description: str):
113
- super().__init__(ActionType.TRANSFORM, description, (TransformResult, ErrorResult, FilterResult))
126
+ super().__init__(ActionType.TRANSFORM, description,
127
+ (TransformResult, TransformResults, ErrorResult, FilterResult))
114
128
 
115
129
  def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
116
130
  return TransformInput(content=delta_file_message.content_list, metadata=delta_file_message.metadata)
117
131
 
118
- def collect(self, transform_inputs: List[TransformInput]):
119
- all_content = []
120
- all_metadata = {}
121
- for transform_input in transform_inputs:
122
- all_content += transform_input.content
123
- all_metadata.update(transform_input.metadata)
124
- return TransformInput(content=all_content, metadata=all_metadata)
132
+ def execute_join_action(self, event):
133
+ if isinstance(self, Join):
134
+ return self.execute(
135
+ event.context,
136
+ self.join([self.build_input(event.context, delta_file_message)
137
+ for delta_file_message in event.delta_file_messages]),
138
+ self.param_class().model_validate(event.params))
139
+ else:
140
+ super().execute_join_action(event)
125
141
 
126
142
  @abstractmethod
127
143
  def transform(self, context: Context, params: BaseModel, transform_input: TransformInput):
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -20,7 +20,9 @@ from enum import Enum
20
20
 
21
21
 
22
22
  class ActionType(Enum):
23
+ INGRESS = "INGRESS"
23
24
  TIMED_INGRESS = "TIMED_INGRESS"
24
25
  TRANSFORM = "TRANSFORM"
25
26
  EGRESS = "EGRESS"
27
+ PUBLISH = "PUBLISH"
26
28
  UNKNOWN = "UNKNOWN"
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import copy
20
20
  from datetime import datetime, timedelta, timezone
21
21
  from logging import Logger
22
22
  from typing import Dict, List, NamedTuple
23
+ from uuid import uuid4
23
24
 
24
25
  from deltafi.storage import ContentService, Segment
25
26
 
@@ -40,61 +41,88 @@ class ActionExecution(NamedTuple):
40
41
 
41
42
  class Context(NamedTuple):
42
43
  did: str
43
- action_flow: str
44
+ delta_file_name: str
45
+ data_source: str
46
+ flow_name: str
47
+ flow_id: str
44
48
  action_name: str
45
- source_filename: str
46
- ingress_flow: str
47
- egress_flow: str
48
- system: str
49
+ action_version: str
49
50
  hostname: str
51
+ system_name: str
50
52
  content_service: ContentService
51
- collect: dict = None
52
- collected_dids: List[str] = None
53
+ join: dict = None
54
+ joined_dids: List[str] = None
53
55
  memo: str = None
54
56
  logger: Logger = None
57
+ saved_content: List = []
55
58
 
56
59
  @classmethod
57
- def create(cls, context: dict, hostname: str, content_service: ContentService, logger: Logger):
60
+ def create(cls, context: dict, content_service: ContentService, logger: Logger):
58
61
  did = context['did']
59
- action_name_parts = context['name'].split(".")
60
- action_flow = action_name_parts[0]
61
- action_name = action_name_parts[1]
62
- if 'sourceFilename' in context:
63
- source_filename = context['sourceFilename']
62
+ if 'deltaFileName' in context:
63
+ delta_file_name = context['deltaFileName']
64
64
  else:
65
- source_filename = None
66
- ingress_flow = context['ingressFlow']
67
- if 'egressFlow' in context:
68
- egress_flow = context['egressFlow']
65
+ delta_file_name = None
66
+ if 'dataSource' in context:
67
+ data_source = context['dataSource']
69
68
  else:
70
- egress_flow = None
71
- system = context['systemName']
72
- if 'collect' in context:
73
- collect = context['collect']
69
+ data_source = None
70
+ if 'flowName' in context:
71
+ flow_name = context['flowName']
74
72
  else:
75
- collect = None
76
- if 'collectedDids' in context:
77
- collected_dids = context['collectedDids']
73
+ flow_name = None
74
+ if 'flowId' in context:
75
+ flow_id = context['flowId']
78
76
  else:
79
- collected_dids = None
77
+ flow_id = None
78
+ if 'actionName' in context:
79
+ action_name = context['actionName']
80
+ else:
81
+ action_name = None
82
+ if 'actionVersion' in context:
83
+ action_version = context['actionVersion']
84
+ else:
85
+ action_version = None
86
+ if 'hostname' in context:
87
+ hostname = context['hostname']
88
+ else:
89
+ hostname = None
90
+ if 'systemName' in context:
91
+ system_name = context['systemName']
92
+ else:
93
+ system_name = None
94
+ if 'join' in context:
95
+ join = context['join']
96
+ else:
97
+ join = None
98
+ if 'joinedDids' in context:
99
+ joined_dids = context['joinedDids']
100
+ else:
101
+ joined_dids = None
80
102
  if 'memo' in context:
81
103
  memo = context['memo']
82
104
  else:
83
105
  memo = None
106
+
84
107
  return Context(did=did,
85
- action_flow=action_flow,
108
+ delta_file_name=delta_file_name,
109
+ data_source=data_source,
110
+ flow_name=flow_name,
111
+ flow_id=flow_id,
86
112
  action_name=action_name,
87
- source_filename=source_filename,
88
- ingress_flow=ingress_flow,
89
- egress_flow=egress_flow,
90
- system=system,
113
+ action_version=action_version,
91
114
  hostname=hostname,
92
- content_service=content_service,
93
- collect=collect,
94
- collected_dids=collected_dids,
115
+ system_name=system_name,
116
+ join=join,
117
+ joined_dids=joined_dids,
95
118
  memo=memo,
119
+ content_service=content_service,
120
+ saved_content=[],
96
121
  logger=logger)
97
122
 
123
+ def child_context(self):
124
+ return self._replace(did=str(uuid4()))
125
+
98
126
 
99
127
  class Content:
100
128
  """
@@ -197,7 +225,6 @@ class Content:
197
225
 
198
226
  return new_segments
199
227
 
200
-
201
228
  def get_size(self):
202
229
  """
203
230
  Returns the size of the content in bytes.
@@ -264,6 +291,12 @@ class Content:
264
291
  """
265
292
  self.segments.extend(other_content.segments)
266
293
 
294
+ def get_segment_names(self):
295
+ segment_names = {}
296
+ for seg in self.segments:
297
+ segment_names[seg.id()] = seg
298
+ return segment_names
299
+
267
300
  def __eq__(self, other):
268
301
  if isinstance(other, Content):
269
302
  return (self.name == other.name and
@@ -296,41 +329,17 @@ class Content:
296
329
  content_service=content_service)
297
330
 
298
331
 
299
- class Domain(NamedTuple):
300
- name: str
301
- value: str
302
- media_type: str
303
-
304
- @classmethod
305
- def from_dict(cls, domain: dict):
306
- name = domain['name']
307
- if 'value' in domain:
308
- value = domain['value']
309
- else:
310
- value = None
311
- media_type = domain['mediaType']
312
- return Domain(name=name,
313
- value=value,
314
- media_type=media_type)
315
-
316
-
317
332
  class DeltaFileMessage(NamedTuple):
318
333
  metadata: Dict[str, str]
319
334
  content_list: List[Content]
320
- domains: List[Domain]
321
- enrichments: List[Domain]
322
335
 
323
336
  @classmethod
324
337
  def from_dict(cls, delta_file_message: dict, content_service: ContentService):
325
338
  metadata = delta_file_message['metadata']
326
339
  content_list = [Content.from_dict(content, content_service) for content in delta_file_message['contentList']]
327
- domains = [Domain.from_dict(domain) for domain in delta_file_message['domains']] if 'domains' in delta_file_message else []
328
- enrichments = [Domain.from_dict(domain) for domain in delta_file_message['enrichments']] if 'enrichments' in delta_file_message else []
329
340
 
330
341
  return DeltaFileMessage(metadata=metadata,
331
- content_list=content_list,
332
- domains=domains,
333
- enrichments=enrichments)
342
+ content_list=content_list)
334
343
 
335
344
 
336
345
  class Event(NamedTuple):
@@ -341,9 +350,10 @@ class Event(NamedTuple):
341
350
  return_address: str
342
351
 
343
352
  @classmethod
344
- def create(cls, event: dict, hostname: str, content_service: ContentService, logger: Logger):
345
- delta_file_messages = [DeltaFileMessage.from_dict(delta_file_message, content_service) for delta_file_message in event['deltaFileMessages']]
346
- context = Context.create(event['actionContext'], hostname, content_service, logger)
353
+ def create(cls, event: dict, content_service: ContentService, logger: Logger):
354
+ delta_file_messages = [DeltaFileMessage.from_dict(delta_file_message, content_service) for delta_file_message in
355
+ event['deltaFileMessages']]
356
+ context = Context.create(event['actionContext'], content_service, logger)
347
357
  params = event['actionParams']
348
358
  queue_name = None
349
359
  if 'queueName' in event:
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -23,16 +23,6 @@ class ExpectedContentException(RuntimeError):
23
23
  self.size = size
24
24
 
25
25
 
26
- class MissingDomainException(RuntimeError):
27
- def __init__(self, name):
28
- self.name = name
29
-
30
-
31
- class MissingEnrichmentException(RuntimeError):
32
- def __init__(self, name):
33
- self.name = name
34
-
35
-
36
26
  class MissingMetadataException(RuntimeError):
37
27
  def __init__(self, key):
38
28
  self.key = key
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -22,7 +22,9 @@
22
22
 
23
23
  Provides an empty subclass of pydantic.BaseModel.
24
24
 
25
- Starting Pydantic v2, the BaseModel cannot be directly instantiated. This class provides for instantiation of GenericModel objects that inherit from BaseModel.
25
+ All action parameter classes must inherit pydantic.BaseModel.
26
+
27
+ Starting Pydantic v2, the BaseModel cannot be directly instantiated. This class provides for instantiation of GenericModel objects that inherit from BaseModel.
26
28
 
27
29
  This class does not define fields for validation or any other purpose.
28
30
  """
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -18,14 +18,14 @@
18
18
 
19
19
  import logging
20
20
  import sys
21
- from datetime import datetime
21
+ from datetime import datetime, UTC
22
22
 
23
23
  import json_logging
24
24
 
25
25
 
26
26
  def get_logger(name: str = None) -> logging.Logger:
27
27
  logger = logging.getLogger(name)
28
- logger.setLevel(logging.DEBUG)
28
+ logger.setLevel(logging.INFO)
29
29
  logger.addHandler(logging.StreamHandler(sys.stdout))
30
30
  logger.propagate = False
31
31
 
@@ -42,7 +42,7 @@ def _sanitize_log_msg(record):
42
42
  class JSONLogFormatter(json_logging.JSONLogFormatter):
43
43
 
44
44
  def _format_log_object(self, record, request_util):
45
- utcnow = datetime.utcnow()
45
+ utcnow = datetime.now(UTC)
46
46
 
47
47
  json_log_object = {
48
48
  'timestamp': json_logging.util.iso_time_format(utcnow),
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # DeltaFi - Data transformation and enrichment platform
3
3
  #
4
- # Copyright 2021-2023 DeltaFi Contributors <deltafi@deltafi.org>
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ from typing import Dict, NamedTuple
22
22
  class Metric(NamedTuple):
23
23
  name: str
24
24
  value: int
25
- tags: Dict[str, str]
25
+ tags: Dict[str, str] = {}
26
26
 
27
27
  def json(self):
28
28
  return {