celus-nibbler 10.1.3__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.
- celus_nibbler-10.1.3/LICENSE +19 -0
- celus_nibbler-10.1.3/PKG-INFO +28 -0
- celus_nibbler-10.1.3/celus_nibbler/__init__.py +28 -0
- celus_nibbler-10.1.3/celus_nibbler/__main__.py +241 -0
- celus_nibbler-10.1.3/celus_nibbler/aggregator.py +162 -0
- celus_nibbler-10.1.3/celus_nibbler/conditions.py +227 -0
- celus_nibbler-10.1.3/celus_nibbler/coordinates.py +198 -0
- celus_nibbler-10.1.3/celus_nibbler/data_headers.py +435 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/__init__.py +74 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/base.py +51 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/celus_format.py +97 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/counter.py +437 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/date_based.py +115 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/date_metric_based.py +112 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/generic.py +120 -0
- celus_nibbler-10.1.3/celus_nibbler/definitions/metric_based.py +115 -0
- celus_nibbler-10.1.3/celus_nibbler/eat_and_poop.py +393 -0
- celus_nibbler-10.1.3/celus_nibbler/errors.py +211 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/__init__.py +97 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/base.py +414 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/counter/__init__.py +279 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/counter/c4.py +433 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/counter/c5.py +313 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/counter/c5json.py +113 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/dynamic.py +7 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/__init__.py +0 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/base.py +7 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/celus_format.py +152 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/date_based.py +12 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/date_metric_based.py +83 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/generic.py +25 -0
- celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/metric_based.py +77 -0
- celus_nibbler-10.1.3/celus_nibbler/reader.py +473 -0
- celus_nibbler-10.1.3/celus_nibbler/sources.py +439 -0
- celus_nibbler-10.1.3/celus_nibbler/utils.py +75 -0
- celus_nibbler-10.1.3/celus_nibbler/validators.py +347 -0
- celus_nibbler-10.1.3/celus_nibbler/warnings.py +44 -0
- celus_nibbler-10.1.3/pyproject.toml +99 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2022 BigDigData s.r.o.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: celus-nibbler
|
|
3
|
+
Version: 10.1.3
|
|
4
|
+
Summary: Counter-like data reader and processor.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: parsing,counter5
|
|
7
|
+
Author: Stepan Henek
|
|
8
|
+
Author-email: stepan@bigdigdata.com
|
|
9
|
+
Requires-Python: >=3.8.9,<4.0.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Provides-Extra: xls
|
|
18
|
+
Requires-Dist: celus-nigiri (>=1.3.3,<1.4.0)
|
|
19
|
+
Requires-Dist: diskcache (>=5.6.0,<5.7.0)
|
|
20
|
+
Requires-Dist: isbnlib (>=3.10.14,<3.11.0)
|
|
21
|
+
Requires-Dist: nltk (>=3.8.1,<3.9.0)
|
|
22
|
+
Requires-Dist: openpyxl (>=3.1.2,<3.2.0)
|
|
23
|
+
Requires-Dist: pydantic (>=2.4.2,<2.5.0)
|
|
24
|
+
Requires-Dist: python-dateutil (>=2.8.2,<2.9.0)
|
|
25
|
+
Requires-Dist: typing-extensions (>=4.8.0,<4.9.0)
|
|
26
|
+
Requires-Dist: unidecode (>=1.3.7,<1.4.0)
|
|
27
|
+
Requires-Dist: xlrd (>=2.0.1,<2.1.0) ; extra == "xls"
|
|
28
|
+
Project-URL: Documentation, https://gitlab.com/big-dig-data/celus-nibbler/-/blob/master/README.md
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .eat_and_poop import Poop, PoopOrganizationStats, PoopStats, eat
|
|
2
|
+
from .errors import (
|
|
3
|
+
MultipleParsersFound,
|
|
4
|
+
NibblerError,
|
|
5
|
+
NoParserForPlatformFound,
|
|
6
|
+
NoParserFound,
|
|
7
|
+
NoParserMatchesHeuristics,
|
|
8
|
+
RecordError,
|
|
9
|
+
TableException,
|
|
10
|
+
WrongFileFormatError,
|
|
11
|
+
)
|
|
12
|
+
from .parsers import get_supported_platforms
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'eat',
|
|
16
|
+
'get_supported_platforms',
|
|
17
|
+
'Poop',
|
|
18
|
+
'PoopStats',
|
|
19
|
+
'PoopOrganizationStats',
|
|
20
|
+
'MultipleParsersFound',
|
|
21
|
+
'NibblerError',
|
|
22
|
+
'NoParserFound',
|
|
23
|
+
'NoParserForPlatformFound',
|
|
24
|
+
'NoParserMatchesHeuristics',
|
|
25
|
+
'RecordError',
|
|
26
|
+
'TableException',
|
|
27
|
+
'WrongFileFormatError',
|
|
28
|
+
]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import csv
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import logging.config
|
|
6
|
+
import pathlib
|
|
7
|
+
import sys
|
|
8
|
+
import typing
|
|
9
|
+
from contextlib import nullcontext
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from datetime import date
|
|
12
|
+
from importlib.metadata import distribution
|
|
13
|
+
|
|
14
|
+
from celus_nigiri import CounterRecord
|
|
15
|
+
from unidecode import unidecode
|
|
16
|
+
|
|
17
|
+
from celus_nibbler import Poop, eat
|
|
18
|
+
from celus_nibbler.aggregator import CounterOrdering
|
|
19
|
+
from celus_nibbler.definitions import Definition
|
|
20
|
+
from celus_nibbler.parsers import available_parsers
|
|
21
|
+
from celus_nibbler.parsers.dynamic import gen_parser
|
|
22
|
+
from celus_nibbler.utils import profile
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_definition(path: str) -> Definition:
|
|
26
|
+
with pathlib.Path(path).open() as f:
|
|
27
|
+
definition_data = json.load(f)
|
|
28
|
+
return Definition.parse(definition_data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def gen_description(parsers: typing.List[str]) -> str:
|
|
32
|
+
parsers_list = "\n".join([f" {e}" for e in parsers])
|
|
33
|
+
|
|
34
|
+
return f"""Available Parsers:
|
|
35
|
+
{parsers_list}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def write_batch(
|
|
40
|
+
writer,
|
|
41
|
+
records: typing.List[CounterRecord],
|
|
42
|
+
dimensions: typing.List[str],
|
|
43
|
+
title_ids: typing.List[str],
|
|
44
|
+
months: typing.List[date],
|
|
45
|
+
):
|
|
46
|
+
months_dict = {e: "" for e in months}
|
|
47
|
+
for r in records:
|
|
48
|
+
months_dict[r.start] = r.value
|
|
49
|
+
writer.writerow(
|
|
50
|
+
[r.title, r.organization]
|
|
51
|
+
+ [r.title_ids[ti] for ti in title_ids]
|
|
52
|
+
+ [r.dimension_data[d] for d in dimensions]
|
|
53
|
+
+ [r.metric]
|
|
54
|
+
+ [months_dict[k] for k in sorted(months)]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def gen_argument_parser(parsers: typing.List[str]) -> argparse.ArgumentParser:
|
|
59
|
+
dist = distribution("celus-nibbler")
|
|
60
|
+
version = dist.version if dist else "?"
|
|
61
|
+
description = gen_description(parsers)
|
|
62
|
+
parser = argparse.ArgumentParser(
|
|
63
|
+
prog="nibbler-eat",
|
|
64
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
65
|
+
description=description,
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument("-d", "--debug", dest="debug", action="store_true", default=False)
|
|
68
|
+
parser.add_argument("--version", action="version", version=version)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"-s",
|
|
71
|
+
"--skip-heuristics",
|
|
72
|
+
dest="skip_heuristics",
|
|
73
|
+
action="store_true",
|
|
74
|
+
default=False,
|
|
75
|
+
help="skip heuristic checks",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"-p",
|
|
79
|
+
"--parser",
|
|
80
|
+
action="append",
|
|
81
|
+
default=[],
|
|
82
|
+
help="limit parsers which can be used",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"-S",
|
|
86
|
+
"--show-summary",
|
|
87
|
+
action="store_true",
|
|
88
|
+
default=False,
|
|
89
|
+
help="show per sheet summary",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"-D",
|
|
93
|
+
"--definition",
|
|
94
|
+
action="append",
|
|
95
|
+
default=[],
|
|
96
|
+
help="Reads a definition of a dynamic parser from a file",
|
|
97
|
+
type=read_definition,
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"-P",
|
|
101
|
+
"--platform",
|
|
102
|
+
default="",
|
|
103
|
+
help="Limit platform that can be used",
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"-c",
|
|
107
|
+
"--counter-like-output",
|
|
108
|
+
action="store_true",
|
|
109
|
+
default=False,
|
|
110
|
+
help="Display processed data in counter like format",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"-N",
|
|
114
|
+
"--no-output",
|
|
115
|
+
action="store_true",
|
|
116
|
+
default=False,
|
|
117
|
+
help="Parses the entire file without producing any data output (useful for benchmarks)",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument("file", nargs='*')
|
|
120
|
+
parser.add_argument("--profile", dest="profile", action="store_true", default=False)
|
|
121
|
+
|
|
122
|
+
return parser
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse(options, platform, dynamic_parsers):
|
|
126
|
+
for file in options.file:
|
|
127
|
+
if poops := eat(
|
|
128
|
+
pathlib.Path(file),
|
|
129
|
+
platform or "void",
|
|
130
|
+
parsers=options.parser or None,
|
|
131
|
+
check_platform=bool(platform),
|
|
132
|
+
use_heuristics=not options.skip_heuristics,
|
|
133
|
+
dynamic_parsers=dynamic_parsers,
|
|
134
|
+
):
|
|
135
|
+
for idx, poop in enumerate(poops):
|
|
136
|
+
if not isinstance(poop, Poop):
|
|
137
|
+
print(f"Failed to pick parser for sheet {idx}", file=sys.stderr)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
stat_dict = poop.get_stats().dict()
|
|
141
|
+
if options.show_summary:
|
|
142
|
+
print(f"Parsing sheet {idx}", file=sys.stderr)
|
|
143
|
+
print(f"Months: {stat_dict['months']}", file=sys.stderr)
|
|
144
|
+
print(f"Metrics: {stat_dict['metrics']}", file=sys.stderr)
|
|
145
|
+
print(f"Dimensions: {stat_dict['dimensions']}", file=sys.stderr)
|
|
146
|
+
print(f"Title ids: {stat_dict['title_ids']}", file=sys.stderr)
|
|
147
|
+
|
|
148
|
+
if options.no_output:
|
|
149
|
+
header = None
|
|
150
|
+
elif options.counter_like_output:
|
|
151
|
+
header = (
|
|
152
|
+
["title", "organization"]
|
|
153
|
+
+ poop.title_ids
|
|
154
|
+
+ poop.dimensions
|
|
155
|
+
+ ["metric"]
|
|
156
|
+
+ poop.months
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
header = [
|
|
160
|
+
"start",
|
|
161
|
+
"end",
|
|
162
|
+
"organization",
|
|
163
|
+
"title",
|
|
164
|
+
"metric",
|
|
165
|
+
"dimensions",
|
|
166
|
+
"title_ids",
|
|
167
|
+
"value",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
writer = csv.writer(sys.stdout, dialect="unix")
|
|
171
|
+
if header:
|
|
172
|
+
writer.writerow(header)
|
|
173
|
+
records = (
|
|
174
|
+
CounterOrdering().aggregate(poop.records_basic())
|
|
175
|
+
if options.counter_like_output
|
|
176
|
+
else poop.records_basic()
|
|
177
|
+
)
|
|
178
|
+
last_rec = None
|
|
179
|
+
batch = []
|
|
180
|
+
for idx, record in records:
|
|
181
|
+
if options.no_output:
|
|
182
|
+
# Just go through the records and suppress any output
|
|
183
|
+
pass
|
|
184
|
+
elif options.counter_like_output:
|
|
185
|
+
this_rec = deepcopy(record)
|
|
186
|
+
this_rec.value = -1
|
|
187
|
+
this_rec.start = date(2020, 1, 1)
|
|
188
|
+
this_rec.end = date(2020, 1, 31)
|
|
189
|
+
|
|
190
|
+
if not last_rec or last_rec == this_rec:
|
|
191
|
+
batch.append(record)
|
|
192
|
+
elif batch:
|
|
193
|
+
write_batch(
|
|
194
|
+
writer,
|
|
195
|
+
batch,
|
|
196
|
+
stat_dict["dimensions"].keys(),
|
|
197
|
+
stat_dict["title_ids"],
|
|
198
|
+
stat_dict["months"].keys(),
|
|
199
|
+
)
|
|
200
|
+
batch = [record]
|
|
201
|
+
last_rec = this_rec
|
|
202
|
+
else:
|
|
203
|
+
writer.writerow(record.as_csv())
|
|
204
|
+
|
|
205
|
+
if batch:
|
|
206
|
+
write_batch(
|
|
207
|
+
writer,
|
|
208
|
+
batch,
|
|
209
|
+
stat_dict["dimensions"].keys(),
|
|
210
|
+
stat_dict["title_ids"],
|
|
211
|
+
stat_dict["months"].keys(),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def main():
|
|
216
|
+
parser = gen_argument_parser(available_parsers())
|
|
217
|
+
options = parser.parse_args()
|
|
218
|
+
|
|
219
|
+
# init logging
|
|
220
|
+
logging.basicConfig(level=logging.DEBUG if options.debug else logging.WARNING)
|
|
221
|
+
logger = logging.getLogger(__name__)
|
|
222
|
+
logger.debug("Logging is configured.")
|
|
223
|
+
|
|
224
|
+
platform = unidecode(options.platform)
|
|
225
|
+
dynamic_parsers = [gen_parser(e) for e in options.definition]
|
|
226
|
+
|
|
227
|
+
if not options.file:
|
|
228
|
+
# Just print help if not parsers specified
|
|
229
|
+
# Enrich with dynamic parsers
|
|
230
|
+
parser = gen_argument_parser(available_parsers(dynamic_parsers))
|
|
231
|
+
parser.print_help()
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
context_man = nullcontext if not options.profile else profile
|
|
235
|
+
|
|
236
|
+
with context_man():
|
|
237
|
+
parse(options, platform, dynamic_parsers)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
main()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import copy
|
|
3
|
+
import tempfile
|
|
4
|
+
import typing
|
|
5
|
+
from collections import deque
|
|
6
|
+
from datetime import date
|
|
7
|
+
|
|
8
|
+
from celus_nigiri import CounterRecord
|
|
9
|
+
from diskcache import Cache
|
|
10
|
+
|
|
11
|
+
from celus_nibbler.errors import NegativeValueInOutput, SameRecordsInOutput
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseAggregator(metaclass=abc.ABCMeta):
|
|
15
|
+
@abc.abstractmethod
|
|
16
|
+
def aggregate(
|
|
17
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
18
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def __or__(self, other):
|
|
22
|
+
return PippedAggregator(self, other)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NoAggregator(BaseAggregator):
|
|
26
|
+
"""Doesn't aggregate anything just passes the data"""
|
|
27
|
+
|
|
28
|
+
def aggregate(
|
|
29
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
30
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
31
|
+
yield from records
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SameAggregator(BaseAggregator):
|
|
35
|
+
"""Aggregates the same records - sums values"""
|
|
36
|
+
|
|
37
|
+
def aggregate(
|
|
38
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
39
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
40
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
41
|
+
with Cache(tmpdir) as db, db.transact():
|
|
42
|
+
# Read all records
|
|
43
|
+
for record in records:
|
|
44
|
+
# Derive key
|
|
45
|
+
new_record = copy.deepcopy(record)
|
|
46
|
+
new_record.value = -1
|
|
47
|
+
key = new_record.as_csv()
|
|
48
|
+
|
|
49
|
+
# Aggregate
|
|
50
|
+
if existing_record := db.get(key):
|
|
51
|
+
existing_record.value += record.value
|
|
52
|
+
db[key] = existing_record
|
|
53
|
+
else:
|
|
54
|
+
db[key] = record
|
|
55
|
+
|
|
56
|
+
for key in db.iterkeys():
|
|
57
|
+
yield db[key]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CheckConflictingRecordsAggregator(BaseAggregator):
|
|
61
|
+
"""
|
|
62
|
+
Checks whether last n records doesn't contain same conflicting records e.g.
|
|
63
|
+
|
|
64
|
+
Title1,Dim1,Metric1,2021-01-01,2021-01-31,999
|
|
65
|
+
Title2,Dim1,Metric1,2021-01-01,2021-01-31,888
|
|
66
|
+
Title1,Dim1,Metric1,2021-01-01,2021-01-31,999
|
|
67
|
+
|
|
68
|
+
is wrong, because it contains same Title, Dimensions and metric for 2020-01 twice
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, size: int = 100):
|
|
73
|
+
self.hash_buffer: typing.Deque[int, int] = deque(maxlen=size)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def make_record_hash(record: CounterRecord):
|
|
77
|
+
return hash(tuple(e for e in record.as_csv()[:-1])) # skip value (last in csv)
|
|
78
|
+
|
|
79
|
+
def aggregate(
|
|
80
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
81
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
82
|
+
idx = 0
|
|
83
|
+
for record in records:
|
|
84
|
+
hsh = self.make_record_hash(record)
|
|
85
|
+
for e in self.hash_buffer:
|
|
86
|
+
if e[0] == hsh:
|
|
87
|
+
raise SameRecordsInOutput(e[1], idx, record)
|
|
88
|
+
self.hash_buffer.append((hsh, idx))
|
|
89
|
+
yield record
|
|
90
|
+
idx += 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CounterOrdering(BaseAggregator):
|
|
94
|
+
"""
|
|
95
|
+
Extracts same records with different dates together e.g.
|
|
96
|
+
|
|
97
|
+
Title1,Dim1,Metric1,2021-01-01,2021-01-31,999
|
|
98
|
+
Title2,Dim1,Metric1,2021-01-01,2021-01-31,888
|
|
99
|
+
Title1,Dim1,Metric1,2021-02-01,2021-02-28,777
|
|
100
|
+
Title2,Dim1,Metric1,2021-02-01,2021-02-28,666
|
|
101
|
+
|
|
102
|
+
->
|
|
103
|
+
|
|
104
|
+
Title1,Dim1,Metric1,2021-01-01,2021-01-31,999
|
|
105
|
+
Title1,Dim1,Metric1,2021-02-01,2021-02-28,777
|
|
106
|
+
Title2,Dim1,Metric1,2021-01-01,2021-01-31,888
|
|
107
|
+
Title2,Dim1,Metric1,2021-02-01,2021-02-28,666
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def aggregate(
|
|
111
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
112
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
114
|
+
with Cache(tmpdir) as db, db.transact():
|
|
115
|
+
# Read all records
|
|
116
|
+
for record in records:
|
|
117
|
+
# Derive key
|
|
118
|
+
new_record = copy.deepcopy(record)
|
|
119
|
+
new_record.value = -1
|
|
120
|
+
# Date aggregation -> fix date in key
|
|
121
|
+
new_record.start = date(2020, 1, 1)
|
|
122
|
+
new_record.end = date(2020, 1, 31)
|
|
123
|
+
key = new_record.as_csv()
|
|
124
|
+
|
|
125
|
+
# Aggregate
|
|
126
|
+
record_dict: typing.Dict[date, CounterRecord] = db.get(key, {})
|
|
127
|
+
if rec := record_dict.get(record.start):
|
|
128
|
+
rec.value += record.value
|
|
129
|
+
else:
|
|
130
|
+
record_dict[record.start] = record
|
|
131
|
+
db[key] = record_dict
|
|
132
|
+
|
|
133
|
+
for db_key in db.iterkeys():
|
|
134
|
+
record_dict = db[db_key]
|
|
135
|
+
yield from [record_dict[key] for key in sorted(record_dict.keys())]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CheckNonNegativeValues(BaseAggregator):
|
|
139
|
+
"""Checks whether outout contains negative values and raises and error if so"""
|
|
140
|
+
|
|
141
|
+
def aggregate(
|
|
142
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
143
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
144
|
+
idx = 0
|
|
145
|
+
for record in records:
|
|
146
|
+
if record.value < 0:
|
|
147
|
+
raise NegativeValueInOutput(idx, record)
|
|
148
|
+
yield record
|
|
149
|
+
idx += 1
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PippedAggregator(BaseAggregator):
|
|
153
|
+
"""Pipes output from one aggregator to another aggregator"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, a1: BaseAggregator, a2: BaseAggregator):
|
|
156
|
+
self.a1 = a1
|
|
157
|
+
self.a2 = a2
|
|
158
|
+
|
|
159
|
+
def aggregate(
|
|
160
|
+
self, records: typing.Generator[CounterRecord, None, None]
|
|
161
|
+
) -> typing.Generator[CounterRecord, None, None]:
|
|
162
|
+
return self.a2.aggregate(self.a1.aggregate(records))
|