vhatable 1.2.1__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.
vhatable/corev2.py ADDED
@@ -0,0 +1,294 @@
1
+ #! /usr/bin/env python
2
+ """TODO"""
3
+
4
+
5
+ # This file is part of Vhatable api.
6
+ #
7
+ # Vhatable api is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # Vhatable api is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with Vhatable api. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ # Copyright 2023 Frédéric MARTIN
21
+ #
22
+ # Contributors list :
23
+ #
24
+ # Frédéric MARTIN frederic.martin.fma@gmail.com
25
+ #
26
+
27
+ import logging
28
+ import time
29
+ from collections import OrderedDict
30
+ from functools import wraps
31
+
32
+
33
+ class Time:
34
+ """TODO"""
35
+
36
+ # pylint: disable=too-few-public-methods
37
+
38
+ def __init__(self, logger_name, label="execution time: {time:.15f}"):
39
+ self.log = logging.getLogger(logger_name)
40
+ self.enabled = False
41
+ self.label = label
42
+
43
+ def __call__(self, original_func):
44
+ @wraps(original_func)
45
+ def time_wrapper(*args, **kwargs):
46
+ self.log.debug("args: %s", args)
47
+ self.log.debug("kwargs: %s", kwargs)
48
+ start = time.time()
49
+ res = original_func(*args, **kwargs)
50
+ end = time.time()
51
+ if len(args) > 0:
52
+ first_arg = args[0]
53
+ self.enabled = getattr(first_arg, "verbose", self.enabled)
54
+ diff = end - start
55
+ msg = self.label.format(time=diff)
56
+ self.log.debug(msg)
57
+ if self.enabled:
58
+ print(msg)
59
+ return res
60
+
61
+ return time_wrapper
62
+
63
+
64
+ class AutoTime:
65
+ """TODO"""
66
+
67
+ # pylint: disable=too-few-public-methods
68
+
69
+ def __init__(self, label="execution time: {time:.15f}"):
70
+ self.enabled = False
71
+ self.label = label
72
+
73
+ def __call__(self, original_func):
74
+ @wraps(original_func)
75
+ def time_wrapper(*args, **kwargs):
76
+ logger_name = args[0].__class__.__name__
77
+ logger_name += "." + original_func.__name__
78
+ log = logging.getLogger(logger_name)
79
+ log.debug("args: %s", args)
80
+ log.debug("kwargs: %s", kwargs)
81
+ start = time.time()
82
+ res = original_func(*args, **kwargs)
83
+ end = time.time()
84
+ if len(args) > 0:
85
+ first_arg = args[0]
86
+ self.enabled = getattr(first_arg, "verbose", self.enabled)
87
+ diff = end - start
88
+ msg = self.label.format(time=diff)
89
+ log.debug(msg)
90
+ if self.enabled:
91
+ print(msg)
92
+ return res
93
+
94
+ return time_wrapper
95
+
96
+
97
+ class Unit:
98
+ """TODO"""
99
+
100
+ def __init__(self, typ="transform", debug=0, identifier=None):
101
+ self.log = logging.getLogger(__name__)
102
+ self._debug = debug
103
+ self._verbose = False
104
+ self.identifier = identifier
105
+ self._attrs = []
106
+ self._gattrs = ["debug", "verbose"]
107
+ self._attrs_ro = []
108
+ self.typ = typ
109
+
110
+ @property
111
+ def debug(self):
112
+ """TODO"""
113
+ return self._debug
114
+
115
+ @debug.setter
116
+ def debug(self, debug):
117
+ """TODO"""
118
+ self._debug = debug
119
+
120
+ @property
121
+ def verbose(self):
122
+ """TODO"""
123
+ return self._verbose
124
+
125
+ @verbose.setter
126
+ def verbose(self, verbose):
127
+ """TODO"""
128
+ self._verbose = verbose
129
+
130
+ def setup(self, from_args):
131
+ """TODO"""
132
+ for attr in self._gattrs:
133
+ if hasattr(from_args, attr):
134
+ setattr(self, attr, getattr(from_args, attr))
135
+ self.log.debug("setup:gattr: %s : %s", attr, getattr(self, attr))
136
+ for attr in self._attrs:
137
+ if self.identifier:
138
+ attr_with_prefix = self.identifier + "_" + attr
139
+ if hasattr(from_args, attr_with_prefix):
140
+ setattr(self, attr, getattr(from_args, attr_with_prefix))
141
+ else:
142
+ if hasattr(from_args, attr):
143
+ setattr(self, attr, getattr(from_args, attr))
144
+ self.log.debug("setup:attr: %s : %s", attr, getattr(self, attr))
145
+ return self
146
+
147
+ def __str__(self):
148
+ rows = []
149
+ prefix = ""
150
+ if self.identifier:
151
+ prefix = self.identifier + "_"
152
+
153
+ rows.append("Unit: " + self.__class__.__name__ + ":")
154
+ for attr in self._gattrs:
155
+ attr_val = getattr(self, attr)
156
+ rows.append(f" - global: {attr}: {attr_val}")
157
+ for attr in self._attrs:
158
+ attr_val = getattr(self, attr)
159
+ rows.append(f" - {prefix}{attr}: {attr_val}")
160
+ for attr in self._attrs_ro:
161
+ attr_val = getattr(self, attr)
162
+ rows.append(f" - readonly: {prefix}{attr}: {attr_val}")
163
+ rows.append("")
164
+ return "\n".join(rows)
165
+
166
+ def __call__(self, rows):
167
+ raise NotImplementedError()
168
+
169
+
170
+ class AbstractProcessor:
171
+ """TODO"""
172
+
173
+ # pylint: disable=too-many-instance-attributes
174
+
175
+ def __init__(
176
+ self,
177
+ debug=0,
178
+ verbose=False,
179
+ rows=None,
180
+ column_names=None,
181
+ autokeys=True,
182
+ messages=None,
183
+ identifier=None,
184
+ ):
185
+ # pylint: disable=too-many-arguments
186
+ self._attrs = ["debug", "verbose", "column_names"]
187
+ self._attrs_ro = ["identifier", "messages"]
188
+ classname = str(self.__class__.__name__.lower())
189
+ self.log = logging.getLogger("vhatable." + classname)
190
+ self.identifier = identifier
191
+ self.units = OrderedDict()
192
+ self.column_names = []
193
+ self._rows = []
194
+ self._raw_cache = None
195
+ self._post_init = False
196
+ self.messages = {}
197
+ self.messages[self.identifier] = {}
198
+ self.debug = debug
199
+ self.verbose = verbose
200
+ if rows:
201
+ self._rows = rows
202
+ if column_names:
203
+ self.column_names = column_names
204
+ elif autokeys and rows and len(rows) >= 1:
205
+ self.column_names = rows[0].keys()
206
+ self._update_messages(messages)
207
+
208
+ def _update_messages(self, messages):
209
+ if messages and self.identifier in messages:
210
+ messages = messages[self.identifier]
211
+ self.messages[self.identifier].update(messages)
212
+
213
+ def _format_messages(self, key, data):
214
+ return self.messages[self.identifier][key].format(**data)
215
+
216
+ def add_unit(self, unit):
217
+ """TODO"""
218
+ self.units[unit.identifier] = unit
219
+
220
+ def post_init(self):
221
+ """TODO"""
222
+ self._post_init = True
223
+
224
+ def __getattr__(self, attr):
225
+ if attr in self.units:
226
+ return self.units[attr]
227
+ classname = self.__class__.__name__
228
+ raise AttributeError(f"'{classname}' object has no attribute '{attr}'")
229
+
230
+ def __str__(self):
231
+ rows = []
232
+ rows.append("Processor - " + self.__class__.__name__ + ":")
233
+ for attr in self._attrs:
234
+ attr_val = getattr(self, attr)
235
+ rows.append(f" - {attr}: {attr_val}")
236
+ for attr in self._attrs_ro:
237
+ attr_val = getattr(self, attr)
238
+ rows.append(f" - readonly: {attr}: {attr_val}")
239
+ rows.append("")
240
+ for unit in self.units.values():
241
+ rows.append(str(unit))
242
+ return "\n".join(rows)
243
+
244
+ def pre_get_raw_hook(self):
245
+ """When get_raw method is called, this method is called just before
246
+ doing anything. It could be use full to run some extra configuration
247
+ steps.
248
+ """
249
+
250
+ def get_raw(self):
251
+ """TODO"""
252
+ if self._raw_cache is not None:
253
+ if self.debug >= 2:
254
+ self.log.debug("cached rows: %s", self._raw_cache)
255
+ return self._raw_cache
256
+ if not self._post_init:
257
+ raise ValueError("post_init method was not called.")
258
+ self.log.debug("Units: %s", self.units)
259
+ self.log.debug("Rows: %s", self._rows)
260
+ self.pre_get_raw_hook()
261
+ pipeline = self._rows
262
+ for unit in self.units.values():
263
+ if unit.typ == "transform":
264
+ self.log.debug("unit:transform: %s", unit)
265
+ self.log.debug("unit:transform:begin: %s", unit.__class__.__name__)
266
+ pipeline = unit(pipeline)
267
+ self.log.debug("unit:transform:end : %s", unit.__class__.__name__)
268
+ self._raw_cache = list(pipeline)
269
+ if self.debug >= 2:
270
+ self.log.debug("pipeline rows: %s", self._raw_cache)
271
+ return self._raw_cache
272
+
273
+ def load(self, rows):
274
+ """TODO"""
275
+ self._rows = rows
276
+ return self
277
+
278
+ def setup(self, from_args):
279
+ """TODO"""
280
+ self.post_init()
281
+ for attr in self._attrs:
282
+ if hasattr(from_args, attr):
283
+ setattr(self, attr, getattr(from_args, attr))
284
+ self.log.debug("setup:attr: %s : %s", attr, getattr(self, attr))
285
+ for unit in self.units.values():
286
+ unit.setup(from_args)
287
+ return self
288
+
289
+ @Time("vhatable.corev2.run")
290
+ def run(self):
291
+ """TODO"""
292
+ for row in self.get_raw():
293
+ print(row)
294
+ return True
vhatable/demo.py ADDED
@@ -0,0 +1,368 @@
1
+ """TODO"""
2
+
3
+ import argparse
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+
9
+ import requests
10
+
11
+ from vhatable.cellv2 import ComplexCellBuilder
12
+ from vhatable.filters import PartialOr
13
+ from vhatable.filtersv2 import EqualMultipleOr, PartialMultipleAnd
14
+ from vhatable.processors import API, ProcessorBuilder, ProcessorFactory, UpdateProcessor
15
+ from vhatable.units import UpdateRowsUnit
16
+
17
+
18
+ class DemoAPI(API):
19
+ """TODO"""
20
+
21
+ URL = "https://epguides.frecar.no/show/bigbangtheory/"
22
+ CACHEFILENAME = "/tmp/vhatable-demo--cache-big-bang.json"
23
+
24
+ def __init__(self):
25
+ super().__init__()
26
+ self.data = self._load_json()
27
+
28
+ def _persist(self, data):
29
+ # pylint: disable=unspecified-encoding
30
+ with open(self.CACHEFILENAME, "w") as json_file:
31
+ json.dump(data, json_file)
32
+
33
+ def reset(self):
34
+ """remove cache file to reset the current state"""
35
+ os.remove(self.CACHEFILENAME)
36
+
37
+ def _load_json(self):
38
+ """TODO"""
39
+ # pylint: disable=unspecified-encoding
40
+ data = None
41
+ if os.path.isfile(self.CACHEFILENAME):
42
+ with open(self.CACHEFILENAME) as user_file:
43
+ file_contents = user_file.read()
44
+ data = json.loads(file_contents)
45
+ return data
46
+
47
+ data = requests.get(self.URL, timeout=60).json()
48
+ res = {}
49
+ cpt = 0
50
+ for season in data:
51
+ for episode in data[season]:
52
+ cpt += 1
53
+ episode["ID"] = cpt
54
+ episode["notes"] = "TODO"
55
+ res[cpt] = episode
56
+ self._persist(res)
57
+ return res
58
+
59
+ def list(self):
60
+ """TODO"""
61
+ return self.data.values()
62
+
63
+ def column_names(self):
64
+ """TODO"""
65
+ return ["ID", "season", "number", "title", "release_date", "show", "notes"]
66
+
67
+ def delete(self, row):
68
+ """TODO"""
69
+ row_id = str(row["ID"])
70
+ row = self.data.pop(row_id)
71
+ self._persist(self.data)
72
+ return True, row
73
+
74
+ def get(self, row_id):
75
+ row_id = str(row_id)
76
+ row = self.data.get(row_id)
77
+ if row is None:
78
+ raise ValueError("row id not found " + row_id)
79
+ return row
80
+
81
+ def update(self, row):
82
+ row_id = str(row["ID"])
83
+ old_row = self.data.get(row_id)
84
+ if old_row is None:
85
+ raise ValueError("row id not found " + row_id)
86
+ if old_row != row:
87
+ self.data[row_id] = row
88
+ self._persist(self.data)
89
+ return True, row
90
+
91
+
92
+ api = DemoAPI()
93
+
94
+
95
+ def newfactory():
96
+ """TODO"""
97
+ messages = {}
98
+ messages["delete"] = {
99
+ "nothing_to_do": "Nothing to delete.",
100
+ "done": (
101
+ "{prefix}{_position}/{_count}: The episode '{title}' ({ID}) was deleted. ({_time}s)"
102
+ ),
103
+ "done_failure": (
104
+ "{prefix}{_position}/{_count}: Failure "
105
+ "The episode '{title}' ({ID}) was not deleted. ({_time}s)"
106
+ ),
107
+ }
108
+ messages["update"] = {
109
+ "nothing_to_do": "Nothing to update.",
110
+ "done": (
111
+ "{prefix}{_position}/{_count}: The episode '{title}' ({ID}) was updated. ({_time}s)"
112
+ ),
113
+ "done_failure": (
114
+ "{prefix}{_position}/{_count}: Failure "
115
+ "The episode '{title}' ({ID}) was not updated. ({_time}s)"
116
+ ),
117
+ }
118
+ messages["count"] = {"count_elt": "Episodes found: {count}"}
119
+ factory = ProcessorFactory()
120
+ factory.set_default_processor_builder(
121
+ ProcessorBuilder(api=api, autoload=True, messages=messages)
122
+ )
123
+ factory.add_processor(
124
+ "update",
125
+ UpdateProcessor,
126
+ builder=ProcessorBuilder(
127
+ api=api,
128
+ autoload=True,
129
+ # we override default updatable columns,
130
+ # only notes is updatable
131
+ units=[UpdateRowsUnit(["notes"])],
132
+ messages=messages,
133
+ ),
134
+ )
135
+ factory.add_custom_cell(
136
+ ComplexCellBuilder(
137
+ "show", fmt="{title} {imdb_id} {epguide_name}", fmtv="{title} {imdb_id} {epguide_name}"
138
+ )
139
+ )
140
+ factory.set_align_cell("title", "l")
141
+ factory.set_align_cell("show", "c")
142
+ factory.set_header_cell("number", "episode")
143
+ return factory
144
+
145
+
146
+ def add_list_cmd_options(parser):
147
+ """Add all default option to the current parser."""
148
+ parser.add_argument(
149
+ "--count",
150
+ default=False,
151
+ action="store_true",
152
+ help="Count the number of the displayed rows",
153
+ dest="count",
154
+ )
155
+ parser.add_argument(
156
+ "--vertical",
157
+ default=False,
158
+ action="store_true",
159
+ help="Use vertical display",
160
+ dest="vertical",
161
+ )
162
+ parser.add_argument(
163
+ "--json", default=False, action="store_true", help="Display rows as json", dest="json"
164
+ )
165
+ parser.add_argument(
166
+ "--json-raw", default=False, action="store_true", help="Display raw json", dest="json_raw"
167
+ )
168
+ parser.add_argument(
169
+ "--csv", default=False, action="store_true", help="Display rows as csv", dest="csv"
170
+ )
171
+ parser.add_argument(
172
+ "--start", default=0, type=int, help="Only display the N first rows", dest="slice_start"
173
+ )
174
+ parser.add_argument(
175
+ "--end", default=0, type=int, help="Only display the N last rows", dest="slice_end"
176
+ )
177
+ parser.add_argument(
178
+ "--limit",
179
+ default=0,
180
+ type=int,
181
+ help="limit the number of rows to display",
182
+ dest="slice_limit",
183
+ )
184
+ parser.add_argument("--sort-by", help="sort row by column", dest="sort_sort_by")
185
+ parser.add_argument(
186
+ "--sort-reversed",
187
+ help="Reverse order while sorting",
188
+ default=False,
189
+ action="store_true",
190
+ dest="sort_reverse",
191
+ )
192
+
193
+
194
+ def add_list_command(main_subparsers):
195
+ """TODO"""
196
+
197
+ def command(args):
198
+ """This command will remove an episode from the cache."""
199
+ factory = newfactory()
200
+ factory.add_filters(PartialMultipleAnd("title", args.titles))
201
+ factory.add_filters(PartialOr("release_date", args.release_dates))
202
+ factory.add_filters(PartialOr("season", args.seasons))
203
+ factory.add_filters(PartialOr("number", args.episodes))
204
+ factory.add_filters(EqualMultipleOr("ID", args.ids))
205
+ processor = factory.create(args)
206
+ print(processor)
207
+ processor.run()
208
+ print()
209
+
210
+ parser = main_subparsers.add_parser("list")
211
+ parser.set_defaults(func=command)
212
+ add_list_cmd_options(parser)
213
+
214
+ parser.add_argument("titles", nargs="*", help="Filter by title")
215
+
216
+ parser.add_argument(
217
+ "--release-date", action="append", help="Filter by release date", dest="release_dates"
218
+ )
219
+
220
+ parser.add_argument("--season", action="append", help="Filter by season", dest="seasons")
221
+
222
+ parser.add_argument("--id", action="append", type=int, help="Filter by id", dest="ids")
223
+
224
+ parser.add_argument("--episode", action="append", help="Filter by episode", dest="episodes")
225
+
226
+ parser.add_argument(
227
+ "--delete", default=False, action="store_true", help="Delete selected rows", dest="delete"
228
+ )
229
+
230
+ parser.add_argument(
231
+ "--update", default=False, action="store_true", help="update selected rows", dest="update"
232
+ )
233
+
234
+ parser.add_argument(
235
+ "--notes", default=None, action="store", help="update notes column", dest="update_notes"
236
+ )
237
+ parser.add_argument(
238
+ "--update-title",
239
+ default=None,
240
+ action="store",
241
+ help="update title column",
242
+ dest="update_title",
243
+ )
244
+
245
+
246
+ def add_delete_command(main_subparsers):
247
+ """TODO"""
248
+
249
+ def command(args):
250
+ """This command will remove some episode from the list.
251
+ This implementation is not the best since we load the entire of
252
+ episodes and we filter them.
253
+ Another solution: We can use the list of IDs to retrieve episode one by
254
+ one and then build a list of them. No more filtering.
255
+ A more performant implementation.
256
+ """
257
+ factory = newfactory()
258
+ factory.default_processor_identifier = "delete"
259
+ factory.add_filters(EqualMultipleOr("ID", args.ids))
260
+ processor = factory.create(args)
261
+ print(processor)
262
+ processor.run()
263
+ print()
264
+
265
+ parser = main_subparsers.add_parser("delete")
266
+ parser.set_defaults(func=command)
267
+ parser.add_argument("ids", nargs="+", type=int, help="Delete episode by id")
268
+
269
+
270
+ def add_delete_commandv2(main_subparsers):
271
+ """TODO"""
272
+
273
+ def command(args):
274
+ """This command will remove some episode from the list.
275
+ We retrieve episode one by one and then build a list
276
+ of them. No more filtering.
277
+ A more performant implementation.
278
+ """
279
+ rows = []
280
+ for row_id in args.ids:
281
+ rows.append(api.get(row_id))
282
+
283
+ factory = newfactory()
284
+ factory.default_processor_identifier = "delete"
285
+ processor = factory.create(args)
286
+ processor.load(rows)
287
+ print(processor)
288
+ processor.run()
289
+ print()
290
+
291
+ parser = main_subparsers.add_parser("deletev2")
292
+ parser.set_defaults(func=command)
293
+ parser.add_argument("ids", nargs="+", type=int, help="Delete episode by id")
294
+
295
+
296
+ def add_reset_command(main_subparsers):
297
+ """TODO"""
298
+
299
+ def command(args):
300
+ """Reset the local cache"""
301
+ # pylint: disable=unused-argument
302
+ api.reset()
303
+
304
+ parser = main_subparsers.add_parser("reset")
305
+ parser.set_defaults(func=command)
306
+
307
+
308
+ def add_update_command(main_subparsers):
309
+ """TODO"""
310
+
311
+ def command(args):
312
+ """This command will remove some episode from the list.
313
+ We retrieve episode one by one and then build a list
314
+ of them. No more filtering.
315
+ A more performant implementation.
316
+ """
317
+ rows = []
318
+ for row_id in args.ids:
319
+ rows.append(api.get(row_id))
320
+
321
+ factory = newfactory()
322
+ factory.default_processor_identifier = "update"
323
+ processor = factory.create(args)
324
+ processor.load(rows)
325
+ print(processor)
326
+ processor.run()
327
+ print()
328
+
329
+ parser = main_subparsers.add_parser("update")
330
+ parser.set_defaults(func=command)
331
+ parser.add_argument("ids", nargs="+", type=int, help="Update episode by id")
332
+ parser.add_argument(
333
+ "--notes", default=None, action="store", help="update notes column", dest="update_notes"
334
+ )
335
+ parser.add_argument(
336
+ "--title", default=None, action="store", help="update title column", dest="update_title"
337
+ )
338
+
339
+
340
+ def main():
341
+ """Main entrypoint of this sample program."""
342
+ log = logging.getLogger()
343
+ log.setLevel(logging.INFO)
344
+ stdout = logging.StreamHandler(sys.stdout)
345
+ fmt = "%(asctime)s:%(name)s:%(funcName)s:%(levelname)s:%(message)s"
346
+ formatter = logging.Formatter(fmt)
347
+ stdout.setFormatter(formatter)
348
+ log.addHandler(stdout)
349
+
350
+ main_parser = argparse.ArgumentParser(description="Demo cli")
351
+ main_parser.add_argument("-d", "--debug", type=int, default=0, help="debug level", dest="debug")
352
+ main_subparsers = main_parser.add_subparsers()
353
+
354
+ add_list_command(main_subparsers)
355
+ add_delete_command(main_subparsers)
356
+ add_delete_commandv2(main_subparsers)
357
+ add_reset_command(main_subparsers)
358
+ add_update_command(main_subparsers)
359
+
360
+ args = main_parser.parse_args()
361
+ if args.debug >= 1:
362
+ log.setLevel(logging.DEBUG)
363
+ if "func" in args:
364
+ print("Demo: ", args.func.__doc__ + "\n")
365
+ sys.exit(args.func(args))
366
+ else:
367
+ print("ERROR: Missing command. see -h")
368
+ sys.exit(2)