zeed-movs-viewer 0.0.0__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.
movsviewer/__init__.py ADDED
File without changes
@@ -0,0 +1,131 @@
1
+ from contextlib import contextmanager
2
+ from logging import info
3
+ from os import listdir
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+ from typing import TYPE_CHECKING
7
+
8
+ from selenium.webdriver import Firefox
9
+ from selenium.webdriver.common.by import By
10
+ from selenium.webdriver.common.keys import Keys
11
+ from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
12
+ from selenium.webdriver.firefox.options import Options
13
+ from selenium.webdriver.firefox.service import Service
14
+ from selenium.webdriver.remote.webelement import WebElement
15
+ from selenium.webdriver.support.expected_conditions import all_of
16
+ from selenium.webdriver.support.expected_conditions import (
17
+ element_to_be_clickable,
18
+ )
19
+ from selenium.webdriver.support.expected_conditions import (
20
+ invisibility_of_element,
21
+ )
22
+ from selenium.webdriver.support.expected_conditions import (
23
+ presence_of_element_located,
24
+ )
25
+ from selenium.webdriver.support.expected_conditions import url_contains
26
+ from selenium.webdriver.support.select import Select
27
+ from selenium.webdriver.support.wait import WebDriverWait
28
+
29
+ from movsviewer.constants import GECKODRIVER_PATH
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import Callable
33
+ from collections.abc import Iterator
34
+
35
+ from selenium.webdriver.remote.webdriver import WebDriver
36
+
37
+
38
+ def get_options(dtemp: str) -> Options:
39
+ options = Options()
40
+
41
+ options.profile = FirefoxProfile()
42
+ # set download folder
43
+ options.profile.set_preference('browser.download.folderList', 2)
44
+ options.profile.set_preference('browser.download.dir', dtemp)
45
+
46
+ return options
47
+
48
+
49
+ def _w(
50
+ wait: 'WebDriverWait[WebDriver]',
51
+ condition: """Callable[[tuple[str, str]],
52
+ Callable[[WebDriver], bool | WebElement]]""",
53
+ css_selector: str,
54
+ ) -> WebElement:
55
+ ret = wait.until(condition((By.CSS_SELECTOR, css_selector)))
56
+ if not isinstance(ret, WebElement):
57
+ raise TypeError
58
+ return ret
59
+
60
+
61
+ def _c(wait: 'WebDriverWait[WebDriver]', css_selector: str) -> WebElement:
62
+ return _w(wait, element_to_be_clickable, css_selector)
63
+
64
+
65
+ def _p(wait: 'WebDriverWait[WebDriver]', css_selector: str) -> WebElement:
66
+ return _w(wait, presence_of_element_located, css_selector)
67
+
68
+
69
+ def _i(wait: 'WebDriverWait[WebDriver]', css_selector: str) -> WebElement:
70
+ return _w(wait, invisibility_of_element, css_selector)
71
+
72
+
73
+ def pl(wait: 'WebDriverWait[WebDriver]', wd: 'WebDriver') -> None:
74
+ _p(wait, '.pageLoader')
75
+ founds = wd.find_elements(By.CSS_SELECTOR, '.pageLoader')
76
+ wait.until(all_of(*(invisibility_of_element(found) for found in founds)))
77
+
78
+
79
+ HP = 'https://bancoposta.poste.it/bpol/public/BPOL_ListaMovimentiAPP/index.html'
80
+
81
+
82
+ @contextmanager
83
+ def get_movimenti(
84
+ username: str, password: str, num_conto: str, get_otp: 'Callable[[], str]'
85
+ ) -> 'Iterator[Path]':
86
+ with (
87
+ TemporaryDirectory() as dtemp,
88
+ Firefox(
89
+ service=Service(executable_path=str(GECKODRIVER_PATH)),
90
+ options=get_options(dtemp),
91
+ ) as wd
92
+ ): # fmt: skip
93
+ wait = WebDriverWait(wd, 1000)
94
+ # login
95
+ wd.get(HP)
96
+ pl(wait, wd)
97
+ wd.find_element(By.CSS_SELECTOR, '#username').send_keys(username)
98
+ wd.find_element(By.CSS_SELECTOR, '#password').send_keys(
99
+ password + Keys.RETURN
100
+ )
101
+ wait.until(
102
+ url_contains(
103
+ 'https://idp-poste.poste.it/jod-idp-retail/cas/app.html'
104
+ )
105
+ )
106
+ pl(wait, wd)
107
+ _c(wait, '#_prosegui').click()
108
+ otp = get_otp()
109
+ wd.find_element(By.CSS_SELECTOR, '#otp').send_keys(otp + Keys.RETURN)
110
+
111
+ # choose conto and download text
112
+ _p(wait, f'select.numconto>option[value="string:{num_conto}"]')
113
+ pl(wait, wd)
114
+ Select(_p(wait, 'select.numconto')).select_by_value(
115
+ f'string:{num_conto}'
116
+ )
117
+
118
+ # hide cookie banner
119
+ wd.execute_script(
120
+ 'document.querySelector("#content-alert-cookie")'
121
+ '.style.display="none"'
122
+ )
123
+ _c(wait, '#select>option[value=TESTO]')
124
+ Select(_p(wait, '#select')).select_by_value('TESTO')
125
+
126
+ info('prima: %s', listdir(dtemp))
127
+ _c(wait, '#downloadApi').click()
128
+ _i(wait, '.waiting')
129
+ info('dopo: %s', listdir(dtemp))
130
+
131
+ yield Path(dtemp) / 'ListaMovimenti.txt'
@@ -0,0 +1,456 @@
1
+ from abc import abstractmethod
2
+ from dataclasses import dataclass
3
+ from datetime import UTC
4
+ from datetime import date
5
+ from datetime import datetime
6
+ from datetime import time
7
+ from decimal import Decimal
8
+ from itertools import accumulate
9
+ from itertools import chain
10
+ from itertools import groupby
11
+ from typing import TYPE_CHECKING
12
+ from typing import NamedTuple
13
+ from typing import Self
14
+ from typing import cast
15
+ from typing import override
16
+
17
+ from guilib.chartwidget.chartwidget import ChartWidget
18
+ from guilib.chartwidget.modelgui import SeriesModel
19
+ from guilib.chartwidget.modelgui import SeriesModelUnit
20
+ from guilib.chartwidget.viewmodel import SortFilterViewModel
21
+ from guilib.dates.converters import date2days
22
+ from guilib.dates.converters import date2QDateTime
23
+ from movslib.model import ZERO
24
+ from PySide6.QtCharts import QBarCategoryAxis
25
+ from PySide6.QtCharts import QBarSeries
26
+ from PySide6.QtCharts import QBarSet
27
+ from PySide6.QtCharts import QCategoryAxis
28
+ from PySide6.QtCharts import QChart
29
+ from PySide6.QtCharts import QLineSeries
30
+ from PySide6.QtCharts import QValueAxis
31
+ from PySide6.QtCore import Qt
32
+
33
+ from movsviewer.reader import read
34
+
35
+ if TYPE_CHECKING:
36
+ from collections.abc import Iterable
37
+ from collections.abc import Sequence
38
+
39
+ from guilib.chartwidget.model import Column
40
+ from guilib.chartwidget.model import ColumnHeader
41
+ from guilib.chartwidget.model import Info
42
+ from movslib.model import Row
43
+ from PySide6.QtWidgets import QGraphicsSceneMouseEvent
44
+ from PySide6.QtWidgets import QGraphicsSceneWheelEvent
45
+
46
+
47
+ class Point(NamedTuple):
48
+ data: date
49
+ mov: Decimal
50
+
51
+
52
+ def to_point(row: 'Row') -> Point:
53
+ if row.accrediti is not None:
54
+ mov = row.accrediti
55
+ elif row.addebiti is not None:
56
+ mov = -row.addebiti
57
+ else:
58
+ mov = ZERO
59
+ return Point(row.date, mov)
60
+
61
+
62
+ def build_series(
63
+ data: 'Sequence[Row]', epoch: date = date(2008, 1, 1)
64
+ ) -> QLineSeries:
65
+ data = sorted(data, key=lambda row: row.date)
66
+
67
+ series = QLineSeries()
68
+ series.setName('data')
69
+
70
+ # add start and today
71
+ moves = chain(
72
+ (Point(epoch, ZERO),),
73
+ map(to_point, data),
74
+ (Point(datetime.now(tz=UTC).date(), ZERO),),
75
+ )
76
+
77
+ def sumy(a: Point, b: Point) -> Point:
78
+ return Point(b.data, a.mov + b.mov)
79
+
80
+ summes = accumulate(moves, func=sumy)
81
+
82
+ floats = (
83
+ (datetime.combine(data, time()).timestamp() * 1000, mov)
84
+ for data, mov in summes
85
+ )
86
+
87
+ # step the movements
88
+ last_mov: Decimal | None = None
89
+ for ts, mov in floats:
90
+ if last_mov is not None:
91
+ series.append(ts, float(last_mov))
92
+ series.append(ts, float(mov))
93
+ last_mov = mov
94
+
95
+ return series
96
+
97
+
98
+ def build_group_by_year_series(
99
+ data: 'Sequence[Row]',
100
+ ) -> tuple[QBarSeries, QBarCategoryAxis]:
101
+ data = sorted(data, key=lambda row: row.date)
102
+
103
+ axis_x = QBarCategoryAxis()
104
+
105
+ series = QBarSeries()
106
+ barset = QBarSet('group by year')
107
+ series.append(barset)
108
+
109
+ def sum_points(points: 'Iterable[Point]') -> Decimal:
110
+ return sum((point.mov for point in points), start=ZERO)
111
+
112
+ years = []
113
+ for year, points in groupby(
114
+ map(to_point, data), lambda point: point.data.year
115
+ ):
116
+ barset.append(float(sum_points(points)))
117
+ years.append(f'{year}')
118
+ axis_x.setCategories(years)
119
+
120
+ return series, axis_x
121
+
122
+
123
+ def build_group_by_month_series(
124
+ data: 'Sequence[Row]',
125
+ ) -> tuple[QBarSeries, QBarCategoryAxis]:
126
+ data = sorted(data, key=lambda row: row.date)
127
+
128
+ axis_x = QBarCategoryAxis()
129
+
130
+ series = QBarSeries()
131
+ barset = QBarSet('group by month')
132
+ series.append(barset)
133
+
134
+ def sum_points(points: 'Iterable[Point]') -> Decimal:
135
+ return sum((point.mov for point in points), start=ZERO)
136
+
137
+ year_months = []
138
+ for (year, month), points in groupby(
139
+ map(to_point, data), lambda point: (point.data.year, point.data.month)
140
+ ):
141
+ barset.append(float(sum_points(points)))
142
+ year_months.append(f'{year}-{month}')
143
+ axis_x.setCategories(year_months)
144
+
145
+ return series, axis_x
146
+
147
+
148
+ class Chart(QChart):
149
+ def __init__(self, data: 'Sequence[Row]') -> None:
150
+ super().__init__()
151
+
152
+ def years(data: 'Sequence[Row]') -> list[date]:
153
+ if not data:
154
+ return []
155
+ data = sorted(data, key=lambda row: row.date)
156
+ start = data[0].date.year - 1
157
+ end = data[-1].date.year + 1
158
+ return [date(year, 1, 1) for year in range(start, end + 1)]
159
+
160
+ def months(data: 'Sequence[Row]', step: int = 1) -> list[date]:
161
+ if not data:
162
+ return []
163
+ data = sorted(data, key=lambda row: row.date)
164
+ start = data[0].date.year - 1
165
+ end = data[-1].date.year + 1
166
+ return [
167
+ date(year, month, 1)
168
+ for year in range(start, end + 1)
169
+ for month in range(1, 13, step)
170
+ ]
171
+
172
+ def reset_axis_x_labels() -> None:
173
+ if True:
174
+ pass
175
+
176
+ def ts(d: date) -> float:
177
+ return (
178
+ datetime(d.year, d.month, d.day, tzinfo=UTC).timestamp() * 1000
179
+ )
180
+
181
+ axis_y = QValueAxis()
182
+ axis_y.setTickType(QValueAxis.TickType.TicksDynamic)
183
+ axis_y.setTickAnchor(0.0)
184
+ axis_y.setTickInterval(10000.0)
185
+ axis_y.setMinorTickCount(9)
186
+ self.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft)
187
+
188
+ axis_x = QCategoryAxis()
189
+ axis_x.setLabelsPosition(
190
+ QCategoryAxis.AxisLabelsPosition.AxisLabelsPositionOnValue
191
+ )
192
+ for dt in months(data, 6):
193
+ axis_x.append(f'{dt}', ts(dt))
194
+ self.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom)
195
+
196
+ series = build_series(data)
197
+ self.addSeries(series)
198
+ series.attachAxis(axis_x)
199
+ series.attachAxis(axis_y)
200
+
201
+ group_by_year_series, axis_x_years = build_group_by_year_series(data)
202
+ self.addSeries(group_by_year_series)
203
+ self.addAxis(axis_x_years, Qt.AlignmentFlag.AlignBottom)
204
+ group_by_year_series.attachAxis(axis_y)
205
+
206
+ group_by_month_series, axis_x_months = build_group_by_month_series(data)
207
+ self.addSeries(group_by_month_series)
208
+ self.addAxis(axis_x_months, Qt.AlignmentFlag.AlignBottom)
209
+ group_by_month_series.attachAxis(axis_y)
210
+
211
+ @override
212
+ def wheelEvent(self, event: 'QGraphicsSceneWheelEvent') -> None:
213
+ super().wheelEvent(event)
214
+ y = event.delta()
215
+ if y < 0:
216
+ self.zoom(0.75) # zoomOut is ~ .5
217
+ elif y > 0:
218
+ self.zoomIn()
219
+
220
+ @override
221
+ def mousePressEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
222
+ super().mousePressEvent(event)
223
+ event.accept()
224
+
225
+ @override
226
+ def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
227
+ super().mouseMoveEvent(event)
228
+
229
+ x_curr, y_curr = cast(tuple[float, float], event.pos().toTuple())
230
+ x_prev, y_prev = cast(tuple[float, float], event.lastPos().toTuple())
231
+ self.scroll(x_prev - x_curr, y_curr - y_prev)
232
+
233
+
234
+ class CH:
235
+ def __init__(self, name: str) -> None:
236
+ self.name = name
237
+
238
+ @override
239
+ def __eq__(self, other: object) -> bool:
240
+ if not isinstance(other, CH):
241
+ return NotImplemented
242
+ return self.name == other.name
243
+
244
+
245
+ class C:
246
+ def __init__(
247
+ self, header: 'ColumnHeader', howmuch: 'Decimal | None'
248
+ ) -> None:
249
+ self.header = header
250
+ self.howmuch = howmuch
251
+
252
+
253
+ class I: # noqa: E742
254
+ def __init__(self, when: 'date', columns: 'Sequence[Column]') -> None:
255
+ self.when = when
256
+ self.columns = columns
257
+
258
+ def howmuch(self, column_header: 'ColumnHeader') -> 'Decimal | None':
259
+ for column in self.columns:
260
+ if column.header == column_header:
261
+ return column.howmuch
262
+ return None
263
+
264
+
265
+ MONEY_HEADER = CH('money')
266
+
267
+
268
+ @dataclass
269
+ class SMFShared:
270
+ x_min: date
271
+ x_max: date
272
+ y_min: Decimal
273
+ y_max: Decimal
274
+
275
+
276
+ class SMFHelper:
277
+ def __init__(self, line_series: QLineSeries, shared: SMFShared) -> None:
278
+ self.line_series = line_series
279
+ self.shared = shared
280
+ self.acc = 0.0
281
+ self.last: date | None = None
282
+
283
+ @abstractmethod
284
+ def step(
285
+ self, when: date, when_d: int, howmuch: Decimal, howmuch_f: float
286
+ ) -> None: ...
287
+
288
+
289
+ class SMFMoney(SMFHelper):
290
+ @override
291
+ def __init__(self, line_series: QLineSeries, shared: SMFShared) -> None:
292
+ super().__init__(line_series, shared)
293
+ self.line_series.setName('money')
294
+
295
+ @override
296
+ def step(
297
+ self,
298
+ when: date, # @UnusedVariable
299
+ when_d: int,
300
+ howmuch: Decimal,
301
+ howmuch_f: float,
302
+ ) -> None:
303
+ self.line_series.append(when_d, 0)
304
+ # TODO @me: fix hover to deal with a variable number of items in series
305
+ # TOD000
306
+ self.line_series.append(when_d, howmuch_f)
307
+
308
+ self.shared.y_min = min(howmuch, self.shared.y_min)
309
+ self.shared.y_max = max(self.shared.y_max, howmuch)
310
+
311
+
312
+ class SMFMoneyAcc(SMFHelper):
313
+ @override
314
+ def __init__(self, line_series: QLineSeries, shared: SMFShared) -> None:
315
+ super().__init__(line_series, shared)
316
+ self.line_series.setName('money acc')
317
+
318
+ @override
319
+ def step(
320
+ self,
321
+ when: date, # @UnusedVariable
322
+ when_d: int,
323
+ howmuch: Decimal, # @UnusedVariable
324
+ howmuch_f: float,
325
+ ) -> None:
326
+ self.line_series.append(when_d, self.acc)
327
+ self.acc += howmuch_f
328
+ self.line_series.append(when_d, self.acc)
329
+
330
+ if self.acc < self.shared.y_min:
331
+ self.shared.y_min = Decimal(self.acc)
332
+ if self.shared.y_max < self.acc:
333
+ self.shared.y_max = Decimal(self.acc)
334
+
335
+
336
+ class SMFMoneyByMonth(SMFHelper):
337
+ @override
338
+ def __init__(self, line_series: QLineSeries, shared: SMFShared) -> None:
339
+ super().__init__(line_series, shared)
340
+ self.line_series.setName('money by month')
341
+
342
+ @override
343
+ def step(
344
+ self,
345
+ when: date,
346
+ when_d: int,
347
+ howmuch: Decimal, # @UnusedVariable
348
+ howmuch_f: float,
349
+ ) -> None:
350
+ # money_by_month
351
+ self.line_series.append(when_d, self.acc)
352
+ if self.last is None or self.last.month != when.month:
353
+ self.acc = 0.0
354
+ else:
355
+ self.acc += howmuch_f
356
+ self.line_series.append(when_d, self.acc)
357
+ self.last = when
358
+
359
+ if self.acc < self.shared.y_min:
360
+ self.shared.y_min = Decimal(self.acc)
361
+ if self.shared.y_max < self.acc:
362
+ self.shared.y_max = Decimal(self.acc)
363
+
364
+
365
+ class SMFMoneyByYear(SMFHelper):
366
+ @override
367
+ def __init__(self, line_series: QLineSeries, shared: SMFShared) -> None:
368
+ super().__init__(line_series, shared)
369
+ self.line_series.setName('money by year')
370
+
371
+ @override
372
+ def step(
373
+ self,
374
+ when: date,
375
+ when_d: int,
376
+ howmuch: Decimal, # @UnusedVariable
377
+ howmuch_f: float,
378
+ ) -> None:
379
+ # money_by_year
380
+ self.line_series.append(when_d, self.acc)
381
+ if self.last is None or self.last.year != when.year:
382
+ self.acc = 0.0
383
+ else:
384
+ self.acc += howmuch_f
385
+ self.line_series.append(when_d, self.acc)
386
+ self.last = when
387
+
388
+ if self.acc < self.shared.y_min:
389
+ self.shared.y_min = Decimal(self.acc)
390
+ if self.shared.y_max < self.acc:
391
+ self.shared.y_max = Decimal(self.acc)
392
+
393
+
394
+ def series_model_factory(infos: 'Sequence[Info]') -> 'SeriesModel':
395
+ """Extract money from info; accumulate and step; group by month / year."""
396
+ shared = SMFShared(
397
+ x_min=date.max,
398
+ x_max=date.min,
399
+ y_min=Decimal('inf'),
400
+ y_max=-Decimal('inf'),
401
+ )
402
+
403
+ money = SMFMoney(QLineSeries(), shared)
404
+ money_acc = SMFMoneyAcc(QLineSeries(), shared)
405
+ money_by_month = SMFMoneyByMonth(QLineSeries(), shared)
406
+ money_by_year = SMFMoneyByYear(QLineSeries(), shared)
407
+
408
+ for info in infos:
409
+ when = info.when
410
+ howmuch = info.howmuch(MONEY_HEADER)
411
+ if howmuch is None:
412
+ continue
413
+
414
+ shared.x_min = min(when, shared.x_min)
415
+ shared.x_max = max(when, shared.x_max)
416
+
417
+ when_d = date2days(when)
418
+ howmuch_f = float(howmuch)
419
+
420
+ money.step(when, when_d, howmuch, howmuch_f)
421
+ money_acc.step(when, when_d, howmuch, howmuch_f)
422
+ money_by_month.step(when, when_d, howmuch, howmuch_f)
423
+ money_by_year.step(when, when_d, howmuch, howmuch_f)
424
+
425
+ return SeriesModel(
426
+ [
427
+ money.line_series,
428
+ money_acc.line_series,
429
+ money_by_month.line_series,
430
+ money_by_year.line_series,
431
+ ],
432
+ date2QDateTime(shared.x_min),
433
+ date2QDateTime(shared.x_max),
434
+ float(shared.y_min),
435
+ float(shared.y_max),
436
+ SeriesModelUnit.EURO,
437
+ )
438
+
439
+
440
+ class ChartWidgetWrapper(ChartWidget):
441
+ def __init__(self, data_path: str) -> None:
442
+ self.data_path = data_path
443
+ self.model = SortFilterViewModel()
444
+ super().__init__(self.model, None, series_model_factory, '%d/%m/%Y')
445
+ self.setCursor(Qt.CursorShape.CrossCursor)
446
+ self.reload()
447
+
448
+ def reload(self) -> Self:
449
+ _, data = read(self.data_path)
450
+ # convert data to infos
451
+ infos = [
452
+ I(row.date, [C(MONEY_HEADER, row.money)])
453
+ for row in sorted(data, key=lambda row: row.date)
454
+ ]
455
+ self.model.update(infos)
456
+ return self
@@ -0,0 +1,16 @@
1
+ from pathlib import Path
2
+ from typing import Final
3
+
4
+
5
+ def _resource(filename: str) -> Path:
6
+ return Path(__file__).with_name('resources') / filename
7
+
8
+
9
+ MAINUI_UI_PATH: Final = _resource('mainui.ui')
10
+ SETTINGSUI_UI_PATH: Final = _resource('settingsui.ui')
11
+
12
+ GECKODRIVER_PATH: Final = _resource('geckodriver.exe')
13
+
14
+ SETTINGS_USERNAME: Final = 'username'
15
+ SETTINGS_PASSWORD: Final = 'password'
16
+ SETTINGS_DATA_PATHS: Final = 'dataPaths'
movsviewer/mainui.py ADDED
@@ -0,0 +1,168 @@
1
+ from pathlib import Path
2
+ from sys import argv
3
+ from typing import TYPE_CHECKING
4
+ from typing import Final
5
+ from typing import cast
6
+
7
+ from guilib.multitabs.widget import MultiTabs
8
+ from guilib.searchsheet.widget import SearchSheet
9
+ from PySide6.QtCore import QCoreApplication
10
+ from PySide6.QtCore import QItemSelection
11
+ from PySide6.QtCore import QItemSelectionModel
12
+ from PySide6.QtCore import Qt
13
+ from PySide6.QtQuick import QQuickWindow
14
+ from PySide6.QtQuick import QSGRendererInterface
15
+ from PySide6.QtUiTools import QUiLoader
16
+ from PySide6.QtWidgets import QApplication
17
+ from PySide6.QtWidgets import QDialog
18
+ from PySide6.QtWidgets import QDialogButtonBox
19
+ from PySide6.QtWidgets import QFileDialog
20
+ from PySide6.QtWidgets import QGridLayout
21
+ from PySide6.QtWidgets import QLineEdit
22
+ from PySide6.QtWidgets import QMainWindow
23
+ from PySide6.QtWidgets import QPlainTextEdit
24
+ from PySide6.QtWidgets import QToolButton
25
+ from PySide6.QtWidgets import QWidget
26
+
27
+ from movsviewer.chartview import ChartWidgetWrapper
28
+ from movsviewer.constants import MAINUI_UI_PATH
29
+ from movsviewer.constants import SETTINGSUI_UI_PATH
30
+ from movsviewer.settings import Settings
31
+ from movsviewer.validator import Validator
32
+ from movsviewer.viewmodel import SortFilterViewModel
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Callable
36
+
37
+ from PySide6.QtGui import QAction
38
+
39
+
40
+ _DATA_PATHS_SEPARATOR = '; \n'
41
+
42
+
43
+ class Mainui(QMainWindow):
44
+ actionSettings: 'QAction' # noqa: N815
45
+ actionUpdate: 'QAction' # noqa: N815
46
+ gridLayout: QGridLayout # noqa: N815
47
+ centralwidget: QWidget
48
+
49
+
50
+ class Settingsui(QDialog):
51
+ usernameLineEdit: QLineEdit # noqa: N815
52
+ passwordLineEdit: QLineEdit # noqa: N815
53
+ dataPaths: QPlainTextEdit # noqa: N815
54
+ buttonBox: QDialogButtonBox # noqa: N815
55
+ openFileChooser: QToolButton # noqa: N815
56
+
57
+
58
+ def _set_data_paths(data_paths: QPlainTextEdit, file_names: list[str]) -> None:
59
+ data_paths.setPlainText(_DATA_PATHS_SEPARATOR.join(file_names))
60
+
61
+
62
+ def _get_data_paths(data_paths: QPlainTextEdit) -> list[str]:
63
+ return data_paths.toPlainText().split(_DATA_PATHS_SEPARATOR)
64
+
65
+
66
+ def new_settingsui(settings: Settings) -> Settingsui:
67
+ def save_settings() -> None:
68
+ settings.data_paths = _get_data_paths(settingsui.dataPaths)
69
+ settings.username = settingsui.usernameLineEdit.text()
70
+ settings.password = settingsui.passwordLineEdit.text()
71
+
72
+ def open_data_paths() -> None:
73
+ file_names, _ = QFileDialog.getOpenFileNames(
74
+ settingsui, dir=str(Path.home())
75
+ )
76
+ _set_data_paths(settingsui.dataPaths, file_names)
77
+
78
+ settingsui = cast(Settingsui, QUiLoader().load(SETTINGSUI_UI_PATH))
79
+ settingsui.usernameLineEdit.setText(settings.username)
80
+ settingsui.passwordLineEdit.setText(settings.password)
81
+ _set_data_paths(settingsui.dataPaths, settings.data_paths)
82
+
83
+ settingsui.accepted.connect(save_settings)
84
+ settingsui.openFileChooser.clicked.connect(open_data_paths)
85
+
86
+ return settingsui
87
+
88
+
89
+ class NewMainui:
90
+ sheets_charts: Final[
91
+ dict[str, tuple[SearchSheet, ChartWidgetWrapper, int]]
92
+ ] = {}
93
+
94
+ settings: Settings
95
+ mainui: Mainui
96
+ multi_tabs: MultiTabs
97
+
98
+ def __call__(self, settings: Settings, settingsui: Settingsui) -> QWidget:
99
+ self.settings = settings
100
+ self.mainui = cast(Mainui, QUiLoader().load(MAINUI_UI_PATH))
101
+
102
+ self.multi_tabs = MultiTabs(self.mainui.centralwidget)
103
+ self.mainui.gridLayout.addWidget(self.multi_tabs, 0, 0, 1, 1)
104
+
105
+ self.mainui.actionUpdate.triggered.connect(self.update_helper)
106
+ self.mainui.actionSettings.triggered.connect(settingsui.show)
107
+ settingsui.accepted.connect(self.update_helper)
108
+
109
+ self.update_helper() # on startup load
110
+
111
+ return self.mainui
112
+
113
+ def new_search_sheet(
114
+ self, data_path: str
115
+ ) -> tuple[SearchSheet, SortFilterViewModel]:
116
+ model = SortFilterViewModel(data_path)
117
+ sheet = SearchSheet(None)
118
+ sheet.set_model(model)
119
+ selection_model = sheet.selection_model()
120
+ selection_model.selectionChanged.connect(
121
+ self.update_status_bar(model, selection_model)
122
+ )
123
+ return sheet, model
124
+
125
+ def update_helper(self) -> None:
126
+ if not Validator(self.mainui, self.settings).validate():
127
+ return
128
+
129
+ data_paths = self.settings.data_paths[:]
130
+ for data_path, (sheet, chart, idx) in self.sheets_charts.items():
131
+ if data_path in data_paths:
132
+ data_paths.remove(data_path)
133
+ sheet.reload()
134
+ chart.reload()
135
+ else:
136
+ self.multi_tabs.remove_double_box(idx)
137
+ del self.sheets_charts[data_path]
138
+ for data_path in data_paths:
139
+ sheet, model = self.new_search_sheet(data_path)
140
+ chart = ChartWidgetWrapper(data_path)
141
+ idx = self.multi_tabs.add_double_box(sheet, chart, model.name)
142
+ self.sheets_charts[data_path] = (sheet, chart, idx)
143
+
144
+ def update_status_bar(
145
+ self, model: SortFilterViewModel, selection_model: QItemSelectionModel
146
+ ) -> 'Callable[[QItemSelection, QItemSelection], None]':
147
+ return lambda _selected, _deselected: model.selection_changed(
148
+ selection_model, self.mainui.statusBar()
149
+ )
150
+
151
+
152
+ def main() -> None:
153
+ QCoreApplication.setAttribute(
154
+ Qt.ApplicationAttribute.AA_ShareOpenGLContexts
155
+ )
156
+ QQuickWindow.setGraphicsApi(
157
+ QSGRendererInterface.GraphicsApi.OpenGLRhi # @UndefinedVariable
158
+ )
159
+
160
+ app = QApplication(argv)
161
+ settings = Settings(argv[1:])
162
+ settingsui = new_settingsui(settings)
163
+ new_mainui = NewMainui()
164
+ mainui = new_mainui(settings, settingsui)
165
+
166
+ mainui.show()
167
+
168
+ raise SystemExit(app.exec())
movsviewer/reader.py ADDED
@@ -0,0 +1,42 @@
1
+ from typing import TYPE_CHECKING
2
+ from typing import Protocol
3
+ from typing import overload
4
+
5
+ from movslib.libretto import read_libretto
6
+ from movslib.movs import read_txt
7
+
8
+ if TYPE_CHECKING:
9
+ from movslib.model import KV
10
+ from movslib.model import Row
11
+ from movslib.model import Rows
12
+
13
+
14
+ class Reader(Protocol):
15
+ @overload
16
+ def __call__(self, fn: str) -> 'tuple[KV, list[Row]]': ...
17
+
18
+ @overload
19
+ def __call__(self, fn: str, name: str) -> 'tuple[KV, Rows]': ...
20
+
21
+ def __call__(
22
+ self, fn: str, name: str | None = None
23
+ ) -> 'tuple[KV, list[Row] | Rows]': ...
24
+
25
+
26
+ def _get_reader(fn: str) -> Reader:
27
+ if fn.endswith('.xlsx'):
28
+ return read_libretto
29
+ return read_txt
30
+
31
+
32
+ @overload
33
+ def read(fn: str) -> 'tuple[KV, list[Row]]': ...
34
+
35
+
36
+ @overload
37
+ def read(fn: str, name: str) -> 'tuple[KV, Rows]': ...
38
+
39
+
40
+ def read(fn: str, name: str | None = None) -> 'tuple[KV, list[Row] | Rows]':
41
+ reader = _get_reader(fn)
42
+ return reader(fn) if name is None else reader(fn, name)
Binary file
@@ -0,0 +1,57 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ui version="4.0">
3
+ <class>MainWindow</class>
4
+ <widget class="QMainWindow" name="MainWindow">
5
+ <property name="geometry">
6
+ <rect>
7
+ <x>0</x>
8
+ <y>0</y>
9
+ <width>800</width>
10
+ <height>600</height>
11
+ </rect>
12
+ </property>
13
+ <property name="windowTitle">
14
+ <string>MainWindow</string>
15
+ </property>
16
+ <widget class="QWidget" name="centralwidget">
17
+ <layout class="QGridLayout" name="gridLayout"/>
18
+ </widget>
19
+ <widget class="QMenuBar" name="menubar">
20
+ <property name="geometry">
21
+ <rect>
22
+ <x>0</x>
23
+ <y>0</y>
24
+ <width>800</width>
25
+ <height>25</height>
26
+ </rect>
27
+ </property>
28
+ <widget class="QMenu" name="menu">
29
+ <property name="title">
30
+ <string>&amp;File</string>
31
+ </property>
32
+ <addaction name="actionUpdate"/>
33
+ <addaction name="actionSettings"/>
34
+ <addaction name="action_Merge"/>
35
+ </widget>
36
+ <addaction name="menu"/>
37
+ </widget>
38
+ <widget class="QStatusBar" name="statusbar"/>
39
+ <action name="actionUpdate">
40
+ <property name="text">
41
+ <string>&amp;Update</string>
42
+ </property>
43
+ </action>
44
+ <action name="actionSettings">
45
+ <property name="text">
46
+ <string>&amp;Settings</string>
47
+ </property>
48
+ </action>
49
+ <action name="action_Merge">
50
+ <property name="text">
51
+ <string>&amp;Merge</string>
52
+ </property>
53
+ </action>
54
+ </widget>
55
+ <resources/>
56
+ <connections/>
57
+ </ui>
@@ -0,0 +1,129 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ui version="4.0">
3
+ <class>Dialog</class>
4
+ <widget class="QDialog" name="Dialog">
5
+ <property name="geometry">
6
+ <rect>
7
+ <x>0</x>
8
+ <y>0</y>
9
+ <width>703</width>
10
+ <height>247</height>
11
+ </rect>
12
+ </property>
13
+ <property name="windowTitle">
14
+ <string>Dialog</string>
15
+ </property>
16
+ <layout class="QGridLayout" name="gridLayout">
17
+ <item row="2" column="0">
18
+ <widget class="QDialogButtonBox" name="buttonBox">
19
+ <property name="orientation">
20
+ <enum>Qt::Horizontal</enum>
21
+ </property>
22
+ <property name="standardButtons">
23
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
24
+ </property>
25
+ </widget>
26
+ </item>
27
+ <item row="1" column="0">
28
+ <widget class="QGroupBox" name="groupBox_2">
29
+ <property name="title">
30
+ <string>General</string>
31
+ </property>
32
+ <layout class="QFormLayout" name="formLayout">
33
+ <item row="0" column="0">
34
+ <widget class="QLabel" name="label_3">
35
+ <property name="text">
36
+ <string>Data paths</string>
37
+ </property>
38
+ </widget>
39
+ </item>
40
+ <item row="0" column="1">
41
+ <widget class="QSplitter" name="splitter">
42
+ <property name="orientation">
43
+ <enum>Qt::Horizontal</enum>
44
+ </property>
45
+ <widget class="QPlainTextEdit" name="dataPaths">
46
+ <property name="sizeAdjustPolicy">
47
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
48
+ </property>
49
+ <property name="readOnly">
50
+ <bool>true</bool>
51
+ </property>
52
+ </widget>
53
+ <widget class="QToolButton" name="openFileChooser">
54
+ <property name="text">
55
+ <string>&amp;Open...</string>
56
+ </property>
57
+ </widget>
58
+ </widget>
59
+ </item>
60
+ </layout>
61
+ </widget>
62
+ </item>
63
+ <item row="0" column="0">
64
+ <widget class="QGroupBox" name="groupBox">
65
+ <property name="title">
66
+ <string>Poste</string>
67
+ </property>
68
+ <layout class="QFormLayout" name="formLayout_2">
69
+ <item row="0" column="0">
70
+ <widget class="QLabel" name="label">
71
+ <property name="text">
72
+ <string>Username</string>
73
+ </property>
74
+ </widget>
75
+ </item>
76
+ <item row="0" column="1">
77
+ <widget class="QLineEdit" name="usernameLineEdit"/>
78
+ </item>
79
+ <item row="1" column="0">
80
+ <widget class="QLabel" name="label_2">
81
+ <property name="text">
82
+ <string>Password</string>
83
+ </property>
84
+ </widget>
85
+ </item>
86
+ <item row="1" column="1">
87
+ <widget class="QLineEdit" name="passwordLineEdit"/>
88
+ </item>
89
+ </layout>
90
+ </widget>
91
+ </item>
92
+ </layout>
93
+ </widget>
94
+ <resources/>
95
+ <connections>
96
+ <connection>
97
+ <sender>buttonBox</sender>
98
+ <signal>accepted()</signal>
99
+ <receiver>Dialog</receiver>
100
+ <slot>accept()</slot>
101
+ <hints>
102
+ <hint type="sourcelabel">
103
+ <x>257</x>
104
+ <y>190</y>
105
+ </hint>
106
+ <hint type="destinationlabel">
107
+ <x>157</x>
108
+ <y>199</y>
109
+ </hint>
110
+ </hints>
111
+ </connection>
112
+ <connection>
113
+ <sender>buttonBox</sender>
114
+ <signal>rejected()</signal>
115
+ <receiver>Dialog</receiver>
116
+ <slot>reject()</slot>
117
+ <hints>
118
+ <hint type="sourcelabel">
119
+ <x>325</x>
120
+ <y>190</y>
121
+ </hint>
122
+ <hint type="destinationlabel">
123
+ <x>286</x>
124
+ <y>199</y>
125
+ </hint>
126
+ </hints>
127
+ </connection>
128
+ </connections>
129
+ </ui>
movsviewer/settings.py ADDED
@@ -0,0 +1,51 @@
1
+ from typing import cast
2
+
3
+ from PySide6.QtCore import QSettings
4
+
5
+ from movsviewer.constants import SETTINGS_DATA_PATHS
6
+ from movsviewer.constants import SETTINGS_PASSWORD
7
+ from movsviewer.constants import SETTINGS_USERNAME
8
+
9
+
10
+ class Settings:
11
+ def __init__(self, argv1: list[str]) -> None:
12
+ self.settings = QSettings('ZeeD', 'mypyui')
13
+ self.argv1 = argv1
14
+
15
+ @property
16
+ def username(self) -> str:
17
+ value = self.settings.value(SETTINGS_USERNAME)
18
+ return cast(str, value) if value is not None else ''
19
+
20
+ @username.setter
21
+ def username(self, username: str) -> None:
22
+ self.settings.setValue(SETTINGS_USERNAME, username)
23
+
24
+ @property
25
+ def password(self) -> str:
26
+ value = self.settings.value(SETTINGS_PASSWORD)
27
+ return cast(str, value) if value is not None else ''
28
+
29
+ @password.setter
30
+ def password(self, password: str) -> None:
31
+ self.settings.setValue(SETTINGS_PASSWORD, password)
32
+
33
+ @property
34
+ def data_paths(self) -> list[str]:
35
+ if self.argv1:
36
+ return self.argv1
37
+
38
+ value = self.settings.value(SETTINGS_DATA_PATHS)
39
+
40
+ if value is None:
41
+ return []
42
+ if isinstance(value, str):
43
+ return [value]
44
+ if isinstance(value, list):
45
+ return value
46
+
47
+ raise ValueError(value)
48
+
49
+ @data_paths.setter
50
+ def data_paths(self, data_paths: list[str]) -> None:
51
+ self.settings.setValue(SETTINGS_DATA_PATHS, data_paths)
@@ -0,0 +1,78 @@
1
+ from datetime import UTC
2
+ from datetime import date
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtWidgets import QMessageBox
8
+ from PySide6.QtWidgets import QWidget
9
+
10
+ from movsviewer.reader import read
11
+
12
+ if TYPE_CHECKING:
13
+ from movslib.model import KV
14
+ from movslib.model import Rows
15
+
16
+ from movsviewer.settings import Settings
17
+
18
+
19
+ def validate_saldo(kv: 'KV', csv: 'Rows', messages: list[str]) -> bool:
20
+ messages.append(f'bpol.saldo_al: {kv.saldo_al}')
21
+ if kv.saldo_al:
22
+ ultimo_update = (datetime.now(tz=UTC).date() - kv.saldo_al).days
23
+ messages.append(
24
+ f'ultimo update: {ultimo_update} giorni fa'
25
+ )
26
+ messages.append(
27
+ f'bpol.saldo_contabile: {float(kv.saldo_contabile):_}'
28
+ )
29
+ messages.append(
30
+ f'bpol.saldo_disponibile: {float(kv.saldo_disponibile):_}'
31
+ )
32
+
33
+ s = sum(item.money for item in csv)
34
+ messages.append(f'Σ (item.accredito - item.addebito): {float(s):_}')
35
+ ret = kv.saldo_contabile == s == kv.saldo_disponibile
36
+ if not ret:
37
+ delta = max(
38
+ [abs(kv.saldo_contabile - s), abs(s - kv.saldo_disponibile)]
39
+ )
40
+ messages.append(f'Δ: {float(delta):_}')
41
+ return ret
42
+
43
+
44
+ def validate_dates(csv: 'Rows', messages: list[str]) -> bool:
45
+ data_contabile: date | None = None
46
+ for row in csv:
47
+ if data_contabile is not None and data_contabile < row.data_contabile:
48
+ messages.append(f'{data_contabile} < {row.data_contabile}!')
49
+ return False
50
+ return True
51
+
52
+
53
+ def validate(fn: str, messages: list[str]) -> bool:
54
+ messages.append(fn)
55
+ kv, csv = read(fn, Path(fn).stem)
56
+ return all(
57
+ [validate_saldo(kv, csv, messages), validate_dates(csv, messages)]
58
+ )
59
+
60
+
61
+ class Validator:
62
+ def __init__(self, parent: QWidget, settings: 'Settings') -> None:
63
+ self.parent = parent
64
+ self.settings = settings
65
+
66
+ def validate(self) -> bool:
67
+ for fn in self.settings.data_paths:
68
+ messages: list[str] = []
69
+ if not validate(fn, messages):
70
+ button = QMessageBox.warning(
71
+ self.parent,
72
+ f'{fn} seems has some problems!',
73
+ '\n'.join(messages),
74
+ QMessageBox.StandardButton.Yes
75
+ | QMessageBox.StandardButton.No,
76
+ )
77
+ return button is QMessageBox.StandardButton.Yes
78
+ return True
@@ -0,0 +1,181 @@
1
+ from dataclasses import fields
2
+ from datetime import date
3
+ from decimal import Decimal
4
+ from operator import iadd
5
+ from operator import isub
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+ from typing import Self
9
+ from typing import cast
10
+ from typing import override
11
+
12
+ from guilib.searchsheet.model import SearchableModel
13
+ from movslib.model import ZERO
14
+ from movslib.model import Row
15
+ from movslib.model import Rows
16
+ from PySide6.QtCore import QAbstractTableModel
17
+ from PySide6.QtCore import QItemSelectionModel
18
+ from PySide6.QtCore import QModelIndex
19
+ from PySide6.QtCore import QObject
20
+ from PySide6.QtCore import QPersistentModelIndex
21
+ from PySide6.QtCore import Qt
22
+ from PySide6.QtGui import QBrush
23
+ from PySide6.QtGui import QColor
24
+
25
+ from movsviewer.reader import read
26
+
27
+ if TYPE_CHECKING:
28
+ from PySide6.QtWidgets import QStatusBar
29
+
30
+
31
+ FIELD_NAMES = [field.name for field in fields(Row)]
32
+
33
+ T_FIELDS = date | Decimal | None | str
34
+
35
+
36
+ def _abs(row: Row) -> Decimal:
37
+ if row.addebiti is not None:
38
+ return -row.addebiti
39
+ if row.accrediti is not None:
40
+ return row.accrediti
41
+ return ZERO
42
+
43
+
44
+ T_INDEX = QModelIndex | QPersistentModelIndex
45
+
46
+
47
+ _INDEX = QModelIndex()
48
+
49
+
50
+ class ViewModel(QAbstractTableModel):
51
+ def __init__(self, data: Rows, parent: QObject | None = None) -> None:
52
+ super().__init__(parent)
53
+ self._set_data(data)
54
+
55
+ def _set_data(self, data: Rows) -> None:
56
+ self._data = data
57
+ abs_data = sorted([_abs(row) for row in data])
58
+ self._min = abs_data[0] if abs_data else ZERO
59
+ self._max = abs_data[-1] if abs_data else ZERO
60
+
61
+ @override
62
+ def rowCount(self, _parent: T_INDEX = _INDEX) -> int:
63
+ return len(self._data)
64
+
65
+ @override
66
+ def columnCount(self, _parent: T_INDEX = _INDEX) -> int:
67
+ return len(FIELD_NAMES)
68
+
69
+ @override
70
+ def headerData(
71
+ self,
72
+ section: int,
73
+ orientation: Qt.Orientation,
74
+ role: int = Qt.ItemDataRole.DisplayRole,
75
+ ) -> str | None:
76
+ if role != Qt.ItemDataRole.DisplayRole:
77
+ return None
78
+
79
+ if orientation != Qt.Orientation.Horizontal:
80
+ return None
81
+
82
+ return FIELD_NAMES[section]
83
+
84
+ @override
85
+ def data(
86
+ self,
87
+ index: QModelIndex | QPersistentModelIndex,
88
+ role: int = Qt.ItemDataRole.DisplayRole,
89
+ ) -> T_FIELDS | QBrush | None:
90
+ column = index.column()
91
+ row = index.row()
92
+
93
+ if role == Qt.ItemDataRole.DisplayRole:
94
+ return str(getattr(self._data[row], FIELD_NAMES[column]))
95
+
96
+ if role == Qt.ItemDataRole.BackgroundRole:
97
+ max_, min_, val = self._max, self._min, _abs(self._data[row])
98
+ perc = (
99
+ (val - min_) / (max_ - min_) if max_ != min_ else Decimal(0.5)
100
+ )
101
+
102
+ hue = int(perc * 120) # 0..359 ; red=0, green=120
103
+ saturation = 223 # 0..255
104
+ lightness = 159 # 0..255
105
+
106
+ return QBrush(QColor.fromHsl(hue, saturation, lightness))
107
+
108
+ if role == Qt.ItemDataRole.UserRole:
109
+ return cast(T_FIELDS, getattr(self._data[row], FIELD_NAMES[column]))
110
+
111
+ return None
112
+
113
+ @override
114
+ def sort(
115
+ self, index: int, order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
116
+ ) -> None:
117
+ def key(row: Row) -> date | Decimal | str: # T_FIELDS - None
118
+ e: T_FIELDS = getattr(row, FIELD_NAMES[index])
119
+ if e is None:
120
+ return ZERO
121
+ return e
122
+
123
+ self.layoutAboutToBeChanged.emit()
124
+ try:
125
+ self._data.sort(
126
+ key=key, reverse=order == Qt.SortOrder.DescendingOrder
127
+ )
128
+ finally:
129
+ self.layoutChanged.emit()
130
+
131
+ def load(self, data: Rows) -> None:
132
+ self.beginResetModel()
133
+ try:
134
+ self._set_data(data)
135
+ finally:
136
+ self.endResetModel()
137
+
138
+ @property
139
+ def name(self) -> str:
140
+ return self._data.name
141
+
142
+
143
+ class SortFilterViewModel(SearchableModel):
144
+ def __init__(self, data_path: str) -> None:
145
+ super().__init__(ViewModel(Rows('')))
146
+ self.data_path = data_path
147
+ self.reload()
148
+
149
+ @override
150
+ def sourceModel(self) -> ViewModel:
151
+ return cast(ViewModel, super().sourceModel())
152
+
153
+ @override
154
+ def sort(
155
+ self, column: int, order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
156
+ ) -> None:
157
+ self.sourceModel().sort(column, order)
158
+
159
+ def selection_changed(
160
+ self, selection_model: QItemSelectionModel, statusbar: 'QStatusBar'
161
+ ) -> None:
162
+ addebiti_index = FIELD_NAMES.index('addebiti')
163
+ accrediti_index = FIELD_NAMES.index('accrediti')
164
+
165
+ bigsum = 0
166
+ for column, iop in ((addebiti_index, isub), (accrediti_index, iadd)):
167
+ for index in selection_model.selectedRows(column):
168
+ data = index.data(Qt.ItemDataRole.UserRole)
169
+ if data is not None:
170
+ bigsum = iop(bigsum, data)
171
+
172
+ statusbar.showMessage(f'⅀ = {bigsum}')
173
+
174
+ def reload(self) -> Self:
175
+ _, data = read(self.data_path, Path(self.data_path).stem)
176
+ self.sourceModel().load(data)
177
+ return self
178
+
179
+ @property
180
+ def name(self) -> str:
181
+ return self.sourceModel().name
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.1
2
+ Name: zeed-movs-viewer
3
+ Version: 0.0.0
4
+ Classifier: Programming Language :: Python :: 3 :: Only
5
+ Classifier: Programming Language :: Python :: 3.12
6
+ Project-URL: Homepage, https://github.com/ZeeD/movs-viewer
7
+ Project-URL: Repository, https://github.com/ZeeD/movs-viewer.git
8
+ Requires-Python: ==3.12.*
9
+ Requires-Dist: pyside6!=6.8,!=6.8.0.1,>=6.7
10
+ Requires-Dist: pythonqwt>=0.12.5
11
+ Requires-Dist: selenium>=4.22
12
+ Requires-Dist: zeed-guilib>=0.0.2
13
+ Requires-Dist: zeed-movs-merger>=0
14
+ Requires-Dist: zeed-movslib>=0.0.2
15
+
@@ -0,0 +1,16 @@
1
+ movsviewer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ movsviewer/automation.py,sha256=HwMiywH2YboXB8WyxP8hIoQgNMmNK-Gq_RWtHJh4zBg,4323
3
+ movsviewer/chartview.py,sha256=8XhOiXIo6vjL0uOGBhBQrl18u7q6X9OkdClfZTIhJDY,13470
4
+ movsviewer/constants.py,sha256=06WyNwgQwEkBrkvrryzEtdWV5mRUBycKe9RTe7PQ_mk,428
5
+ movsviewer/mainui.py,sha256=RLxEPoNL3uUxB4ieJemB8-P6-qZX7voUwwqR3rJ_XOA,5717
6
+ movsviewer/reader.py,sha256=8glE4CSv5RMe8ezP3RvKdzLqYhoT-Oy03My1oP5Dmdk,997
7
+ movsviewer/resources/geckodriver.exe,sha256=CCfcGwdNLMMpCWKc05nDO-3q6C88o3sEllUGV4HDGLM,3300792
8
+ movsviewer/resources/mainui.ui,sha256=lCMfw4A0KkVw9KP14raCaGzDKc7Hx2W8OexZxT3npQk,1376
9
+ movsviewer/resources/settingsui.ui,sha256=6eH5C9rkNrPlyp3JCQSW06drKhepwdQ0la4VGiqmPvE,3369
10
+ movsviewer/settings.py,sha256=yMLzNHiWYo9iUQYtRMNvs5UYbt0k4DndW7svNU2O_tg,1462
11
+ movsviewer/validator.py,sha256=huYRRRTIjVQHm6DlbtXVYROp0_SM5_7Vv7WV42d2or4,2560
12
+ movsviewer/viewmodel.py,sha256=ZEDy4I3bcSQh3ewQ_1K_fJ6ZJTJRHmwN0kH4rUPUSnk,5204
13
+ zeed_movs_viewer-0.0.0.dist-info/METADATA,sha256=3iC7EOQhBJ75187XeNUcLIcSA5ymg9hvq-UfTBer0PY,529
14
+ zeed_movs_viewer-0.0.0.dist-info/WHEEL,sha256=pM0IBB6ZwH3nkEPhtcp50KvKNX-07jYtnb1g1m6Z4Co,90
15
+ zeed_movs_viewer-0.0.0.dist-info/entry_points.txt,sha256=Cr9WqD8tBOMTJBuMV6-ayk_L49BmMfrKyZLk51-0008,71
16
+ zeed_movs_viewer-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.4.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+
3
+ [gui_scripts]
4
+ movs-viewer = movsviewer.mainui:main
5
+