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.
Files changed (38) hide show
  1. celus_nibbler-10.1.3/LICENSE +19 -0
  2. celus_nibbler-10.1.3/PKG-INFO +28 -0
  3. celus_nibbler-10.1.3/celus_nibbler/__init__.py +28 -0
  4. celus_nibbler-10.1.3/celus_nibbler/__main__.py +241 -0
  5. celus_nibbler-10.1.3/celus_nibbler/aggregator.py +162 -0
  6. celus_nibbler-10.1.3/celus_nibbler/conditions.py +227 -0
  7. celus_nibbler-10.1.3/celus_nibbler/coordinates.py +198 -0
  8. celus_nibbler-10.1.3/celus_nibbler/data_headers.py +435 -0
  9. celus_nibbler-10.1.3/celus_nibbler/definitions/__init__.py +74 -0
  10. celus_nibbler-10.1.3/celus_nibbler/definitions/base.py +51 -0
  11. celus_nibbler-10.1.3/celus_nibbler/definitions/celus_format.py +97 -0
  12. celus_nibbler-10.1.3/celus_nibbler/definitions/counter.py +437 -0
  13. celus_nibbler-10.1.3/celus_nibbler/definitions/date_based.py +115 -0
  14. celus_nibbler-10.1.3/celus_nibbler/definitions/date_metric_based.py +112 -0
  15. celus_nibbler-10.1.3/celus_nibbler/definitions/generic.py +120 -0
  16. celus_nibbler-10.1.3/celus_nibbler/definitions/metric_based.py +115 -0
  17. celus_nibbler-10.1.3/celus_nibbler/eat_and_poop.py +393 -0
  18. celus_nibbler-10.1.3/celus_nibbler/errors.py +211 -0
  19. celus_nibbler-10.1.3/celus_nibbler/parsers/__init__.py +97 -0
  20. celus_nibbler-10.1.3/celus_nibbler/parsers/base.py +414 -0
  21. celus_nibbler-10.1.3/celus_nibbler/parsers/counter/__init__.py +279 -0
  22. celus_nibbler-10.1.3/celus_nibbler/parsers/counter/c4.py +433 -0
  23. celus_nibbler-10.1.3/celus_nibbler/parsers/counter/c5.py +313 -0
  24. celus_nibbler-10.1.3/celus_nibbler/parsers/counter/c5json.py +113 -0
  25. celus_nibbler-10.1.3/celus_nibbler/parsers/dynamic.py +7 -0
  26. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/__init__.py +0 -0
  27. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/base.py +7 -0
  28. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/celus_format.py +152 -0
  29. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/date_based.py +12 -0
  30. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/date_metric_based.py +83 -0
  31. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/generic.py +25 -0
  32. celus_nibbler-10.1.3/celus_nibbler/parsers/non_counter/metric_based.py +77 -0
  33. celus_nibbler-10.1.3/celus_nibbler/reader.py +473 -0
  34. celus_nibbler-10.1.3/celus_nibbler/sources.py +439 -0
  35. celus_nibbler-10.1.3/celus_nibbler/utils.py +75 -0
  36. celus_nibbler-10.1.3/celus_nibbler/validators.py +347 -0
  37. celus_nibbler-10.1.3/celus_nibbler/warnings.py +44 -0
  38. 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))