QT-PyQt-PySide-Custom-Widgets-Pro 1.1.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.
Files changed (45) hide show
  1. custom_widgets_pro/__init__.py +73 -0
  2. custom_widgets_pro/_catalog.py +25 -0
  3. custom_widgets_pro/_cli.py +132 -0
  4. custom_widgets_pro/_license.py +419 -0
  5. custom_widgets_pro/_version.py +14 -0
  6. custom_widgets_pro/datatable/__init__.py +17 -0
  7. custom_widgets_pro/datatable/datatable_pro.py +319 -0
  8. custom_widgets_pro/datatable/export.py +143 -0
  9. custom_widgets_pro/datatable/frozen_view.py +112 -0
  10. custom_widgets_pro/datatable/grouping.py +309 -0
  11. custom_widgets_pro/datatable/pivot.py +100 -0
  12. custom_widgets_pro/datatable/provider.py +98 -0
  13. custom_widgets_pro/datatable/virtual_model.py +451 -0
  14. custom_widgets_pro/widgets/__init__.py +7 -0
  15. custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
  16. custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
  17. custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
  18. custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
  19. custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
  20. custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
  21. custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
  22. custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
  23. custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
  24. custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
  25. custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
  26. custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
  27. custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
  28. custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
  29. custom_widgets_pro/widgets/charts/__init__.py +1 -0
  30. custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
  31. custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
  32. custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
  33. custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
  34. custom_widgets_pro/widgets/data/__init__.py +1 -0
  35. custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
  36. custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
  37. custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
  38. custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
  39. custom_widgets_pro/widgets/media/__init__.py +1 -0
  40. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
  41. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
  42. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
  43. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
  44. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
  45. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,309 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """Grouping / aggregation engine for DataTable Pro.
6
+
7
+ Pure Python - no Qt - so it is unit-testable on its own and the Qt model
8
+ (virtual_model.VirtualDataTableModel) is a thin adapter over it.
9
+
10
+ The engine turns a flat list of row dicts into a **grouped outline**: an ordered
11
+ list of *visible entries* where each entry is either a ``GroupRow`` (a group
12
+ header carrying the group value, a row count and per-column aggregates) or a
13
+ plain data-row dict. Nested grouping (group-by several keys) produces nested
14
+ headers; each header can be expanded or collapsed independently.
15
+
16
+ This is the "flat outline" strategy (headers interleaved with data rows in one
17
+ flat list) rather than a Qt tree model, so the whole DataTable Pro chain -
18
+ frozen-column overlay, the QTableView, the sort/filter proxy - keeps working
19
+ unchanged; only the model's row set changes shape.
20
+
21
+ Grouping is an aggregate-over-the-whole-result operation, so the caller
22
+ materialises the current (sorted/filtered) result before grouping; when grouping
23
+ is cleared the table returns to its lazy/virtualized flat mode.
24
+
25
+ Aggregates: a spec maps a column key to a reducer - one of the names in
26
+ ``AGGREGATORS`` (sum/avg/count/min/max/first/last) or any callable
27
+ ``reducer(values) -> result`` over the list of that column's raw values in the
28
+ group.
29
+ """
30
+
31
+ AGGREGATORS = ("sum", "avg", "mean", "count", "min", "max", "first", "last")
32
+
33
+
34
+ def _is_number(v):
35
+ return isinstance(v, (int, float)) and not isinstance(v, bool)
36
+
37
+
38
+ def aggregate(reducer, values):
39
+ """Reduce a list of raw cell values to a single aggregate.
40
+
41
+ ``reducer`` is a name from AGGREGATORS or a callable. ``count`` counts all
42
+ rows (including ones whose value is None); every other reducer ignores
43
+ Nones. Returns None when there is nothing to reduce (except sum -> 0 and
44
+ count -> len).
45
+ """
46
+ if callable(reducer):
47
+ return reducer(list(values))
48
+ name = str(reducer).lower()
49
+ if name == "count":
50
+ return len(values)
51
+ non_null = [v for v in values if v is not None]
52
+ nums = [v for v in non_null if _is_number(v)]
53
+ if name == "sum":
54
+ return sum(nums) if nums else 0
55
+ if name in ("avg", "mean"):
56
+ return (sum(nums) / len(nums)) if nums else None
57
+ if name in ("min", "max"):
58
+ if not non_null:
59
+ return None
60
+ try:
61
+ return min(non_null) if name == "min" else max(non_null)
62
+ except TypeError: # mixed, non-orderable types
63
+ return None
64
+ if name == "first":
65
+ return non_null[0] if non_null else None
66
+ if name == "last":
67
+ return non_null[-1] if non_null else None
68
+ raise ValueError("unknown aggregate %r" % (reducer,))
69
+
70
+
71
+ def _hkey(value):
72
+ """A hashable stand-in for a (possibly unhashable) group value, used for
73
+ path identity. The raw value is kept separately for display."""
74
+ try:
75
+ hash(value)
76
+ return value
77
+ except TypeError:
78
+ return ("\0unhashable", repr(value))
79
+
80
+
81
+ class GroupRow(object):
82
+ """A group-header visible entry.
83
+
84
+ depth 0-based nesting level (0 = outermost group key).
85
+ key the column key this level groups by.
86
+ keyTitle that column's display title.
87
+ value the raw group value (what ``key`` equals for this group).
88
+ count number of data rows under this group (all descendants).
89
+ aggregates {column_key: aggregate_value} for the configured aggregate spec.
90
+ path hashable identity tuple from the root to this group.
91
+ expanded whether this group is currently expanded.
92
+ """
93
+
94
+ __slots__ = ("depth", "key", "keyTitle", "value", "count",
95
+ "aggregates", "path", "expanded")
96
+
97
+ def __init__(self, depth, key, keyTitle, value, count, aggregates, path,
98
+ expanded):
99
+ self.depth = depth
100
+ self.key = key
101
+ self.keyTitle = keyTitle
102
+ self.value = value
103
+ self.count = count
104
+ self.aggregates = aggregates
105
+ self.path = path
106
+ self.expanded = expanded
107
+
108
+ def __repr__(self):
109
+ return ("GroupRow(depth=%d, key=%r, value=%r, count=%d, expanded=%r)"
110
+ % (self.depth, self.key, self.value, self.count, self.expanded))
111
+
112
+
113
+ class _Node(object):
114
+ __slots__ = ("depth", "key", "keyTitle", "value", "path",
115
+ "children", "leaves", "aggregates", "count")
116
+
117
+
118
+ class GroupingEngine(object):
119
+ """Builds and maintains the grouped outline for a set of rows.
120
+
121
+ keys ordered list of column keys to group by (outer -> inner).
122
+ aggregates {column_key: reducer} spec (reducer name or callable).
123
+ titles optional {column_key: title} for header labels.
124
+ """
125
+
126
+ def __init__(self, keys, aggregates=None, titles=None):
127
+ self._keys = [k for k in (keys or [])]
128
+ self._aggregates = dict(aggregates or {})
129
+ self._titles = dict(titles or {})
130
+ self._collapsed = set() # set of node paths that are collapsed
131
+ self._roots = [] # top-level _Node list
132
+ self._nodes = {} # path -> _Node
133
+ self.visible = [] # flat list: GroupRow | row-dict
134
+ self._rows = []
135
+
136
+ # ------------------------------------------------------------------ #
137
+ ## Configuration / (re)build
138
+ # ------------------------------------------------------------------ #
139
+ def keys(self):
140
+ return list(self._keys)
141
+
142
+ def setRows(self, rows):
143
+ """(Re)build the whole outline from ``rows``. Collapse state is reset."""
144
+ self._rows = list(rows or [])
145
+ self._collapsed = set()
146
+ self._buildTree()
147
+ self._rebuildVisible()
148
+
149
+ def _title(self, key):
150
+ return self._titles.get(key, key)
151
+
152
+ def _aggregatesFor(self, rows):
153
+ out = {}
154
+ for col_key, reducer in self._aggregates.items():
155
+ out[col_key] = aggregate(reducer, [r.get(col_key) for r in rows])
156
+ return out
157
+
158
+ def _buildTree(self):
159
+ self._nodes = {}
160
+ self._roots = self._buildLevel(self._rows, self._keys, 0, ())
161
+
162
+ def _buildLevel(self, rows, keys, depth, parent_path):
163
+ key = keys[0]
164
+ title = self._title(key)
165
+ order = [] # first-seen group order (stable)
166
+ buckets = {} # hkey -> (raw_value, [rows])
167
+ for r in rows:
168
+ value = r.get(key)
169
+ hk = _hkey(value)
170
+ bucket = buckets.get(hk)
171
+ if bucket is None:
172
+ buckets[hk] = (value, [r])
173
+ order.append(hk)
174
+ else:
175
+ bucket[1].append(r)
176
+ nodes = []
177
+ deeper = keys[1:]
178
+ for hk in order:
179
+ value, grp_rows = buckets[hk]
180
+ path = parent_path + (hk,)
181
+ node = _Node()
182
+ node.depth = depth
183
+ node.key = key
184
+ node.keyTitle = title
185
+ node.value = value
186
+ node.path = path
187
+ node.count = len(grp_rows)
188
+ node.aggregates = self._aggregatesFor(grp_rows)
189
+ if deeper:
190
+ node.children = self._buildLevel(grp_rows, deeper, depth + 1, path)
191
+ node.leaves = None
192
+ else:
193
+ node.children = None
194
+ node.leaves = grp_rows
195
+ self._nodes[path] = node
196
+ nodes.append(node)
197
+ return nodes
198
+
199
+ def _groupRow(self, node):
200
+ return GroupRow(node.depth, node.key, node.keyTitle, node.value,
201
+ node.count, node.aggregates, node.path,
202
+ node.path not in self._collapsed)
203
+
204
+ def _rebuildVisible(self):
205
+ out = []
206
+ for node in self._roots:
207
+ self._flatten(node, out)
208
+ self.visible = out
209
+
210
+ def _flatten(self, node, out):
211
+ out.append(self._groupRow(node))
212
+ if node.path in self._collapsed:
213
+ return
214
+ if node.children is not None:
215
+ for child in node.children:
216
+ self._flatten(child, out)
217
+ else:
218
+ out.extend(node.leaves)
219
+
220
+ # ------------------------------------------------------------------ #
221
+ ## Expand / collapse
222
+ # ------------------------------------------------------------------ #
223
+ def isExpanded(self, path):
224
+ return path not in self._collapsed
225
+
226
+ def _descendantCount(self, node):
227
+ """Number of visible rows shown *below* ``node``'s header when the node
228
+ itself is expanded, honouring the collapse state of its descendants."""
229
+ if node.children is None:
230
+ return len(node.leaves)
231
+ total = 0
232
+ for child in node.children:
233
+ total += 1 # the child's own header
234
+ if child.path not in self._collapsed:
235
+ total += self._descendantCount(child)
236
+ return total
237
+
238
+ def _visibleIndexOf(self, path):
239
+ for i, entry in enumerate(self.visible):
240
+ if isinstance(entry, GroupRow) and entry.path == path:
241
+ return i
242
+ return -1
243
+
244
+ def planToggle(self, path):
245
+ """Describe the row change a toggle of ``path`` will make, WITHOUT
246
+ applying it: ``(kind, start, count)`` where kind is "insert" (the group
247
+ is collapsed and will expand) or "remove" (expanded, will collapse),
248
+ start is the first affected visible row, count the number of rows.
249
+
250
+ Returns None if ``path`` is not a known group. Call applyToggle() to
251
+ commit the change (between a Qt begin/endInsert|RemoveRows bracket)."""
252
+ node = self._nodes.get(path)
253
+ if node is None:
254
+ return None
255
+ header_idx = self._visibleIndexOf(path)
256
+ if header_idx < 0:
257
+ return None
258
+ count = self._descendantCount(node)
259
+ kind = "insert" if path in self._collapsed else "remove"
260
+ return kind, header_idx + 1, count
261
+
262
+ def applyToggle(self, path):
263
+ """Flip the collapse state of ``path`` and rebuild the visible list.
264
+ Returns the new expanded state (True if now expanded)."""
265
+ if path in self._collapsed:
266
+ self._collapsed.discard(path)
267
+ expanded = True
268
+ else:
269
+ self._collapsed.add(path)
270
+ expanded = False
271
+ self._rebuildVisible()
272
+ return expanded
273
+
274
+ def toggle(self, path):
275
+ """Convenience (non-Qt callers): plan + apply in one step. Returns the
276
+ change descriptor from planToggle."""
277
+ plan = self.planToggle(path)
278
+ if plan is not None:
279
+ self.applyToggle(path)
280
+ return plan
281
+
282
+ def setAllExpanded(self, expanded):
283
+ """Expand (True) or collapse (False) every group."""
284
+ if expanded:
285
+ self._collapsed = set()
286
+ else:
287
+ self._collapsed = set(self._nodes.keys())
288
+ self._rebuildVisible()
289
+
290
+ # ------------------------------------------------------------------ #
291
+ ## Aggregate refresh (after an in-place edit of a data row)
292
+ # ------------------------------------------------------------------ #
293
+ def recomputeAggregates(self):
294
+ """Recompute every group's count + aggregates from its current leaf
295
+ rows (leaves are the same dict objects passed in, so an in-place edit is
296
+ reflected). Cheap relative to a full rebuild and keeps collapse state."""
297
+ for node in self._nodes.values():
298
+ leaves = self._leavesOf(node)
299
+ node.count = len(leaves)
300
+ node.aggregates = self._aggregatesFor(leaves)
301
+ self._rebuildVisible()
302
+
303
+ def _leavesOf(self, node):
304
+ if node.children is None:
305
+ return list(node.leaves)
306
+ out = []
307
+ for child in node.children:
308
+ out.extend(self._leavesOf(child))
309
+ return out
@@ -0,0 +1,100 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """Pivot (cross-tab) transform for DataTable Pro.
6
+
7
+ Pure Python - no Qt. A pivot reshapes a flat row set into a matrix: one output
8
+ row per distinct combination of the *index* (row-dimension) keys, one output
9
+ column per distinct value of a single *columns* key, and each cell is the
10
+ *values* key aggregated over the rows in that (row, column) bucket. An optional
11
+ row-total column aggregates across the whole output row.
12
+
13
+ Because the result is just another flat ``(columns, rows)`` pair, the widget
14
+ renders a pivot through the ordinary table path - no special model. The value
15
+ columns are keyed with a namespaced prefix so they can never collide with a real
16
+ data key.
17
+
18
+ Reuses the grouping module's ``aggregate`` reducer (sum/avg/count/min/max/
19
+ first/last or a callable).
20
+ """
21
+ from .grouping import aggregate
22
+
23
+ #: Prefix for generated pivot value-column keys (namespaced with a NUL so it
24
+ #: cannot clash with a genuine row-dict key).
25
+ PIVOT_COL_PREFIX = "\0pivot\0"
26
+ TOTAL_KEY = PIVOT_COL_PREFIX + "__total__"
27
+
28
+
29
+ def _safe_key(value):
30
+ """A total-orderable sort key for an arbitrary cell value (Nones last)."""
31
+ return (value is None, str(value))
32
+
33
+
34
+ def _distinct_sorted(values):
35
+ seen = set()
36
+ out = []
37
+ for v in values:
38
+ k = _safe_key(v)
39
+ if k not in seen:
40
+ seen.add(k)
41
+ out.append(v)
42
+ return sorted(out, key=_safe_key)
43
+
44
+
45
+ def pivot_table(rows, index, columns, values, agg="sum", totals=True,
46
+ fill=None, total_title="Total"):
47
+ """Build a pivot table.
48
+
49
+ rows iterable of row dicts.
50
+ index row-dimension key, or a list of keys (nested row headers).
51
+ columns a single key whose distinct values become output columns.
52
+ values the key aggregated into each cell.
53
+ agg reducer name or callable (see grouping.aggregate).
54
+ totals add a trailing per-row total column.
55
+ fill value for an empty (row, column) cell (default None).
56
+ total_title header text for the total column.
57
+
58
+ Returns ``(col_defs, result_rows)`` where col_defs is a list of column
59
+ descriptor dicts (index columns keep their key as title; value columns are
60
+ titled by their column value) and result_rows is a list of row dicts."""
61
+ rows = list(rows or [])
62
+ index_keys = [index] if isinstance(index, str) else list(index)
63
+
64
+ col_values = _distinct_sorted(r.get(columns) for r in rows)
65
+
66
+ col_defs = [{"key": k, "title": k} for k in index_keys]
67
+ value_cols = []
68
+ for cv in col_values:
69
+ key = PIVOT_COL_PREFIX + str(cv)
70
+ title = "" if cv is None else str(cv)
71
+ col_defs.append({"key": key, "title": title, "type": "number"})
72
+ value_cols.append((key, _safe_key(cv)))
73
+ if totals:
74
+ col_defs.append({"key": TOTAL_KEY, "title": total_title, "type": "number"})
75
+
76
+ # bucket rows by the index-key tuple, in a deterministic sorted order
77
+ order = []
78
+ buckets = {}
79
+ for r in rows:
80
+ tup = tuple(r.get(k) for k in index_keys)
81
+ hk = tuple(_safe_key(x) for x in tup)
82
+ bucket = buckets.get(hk)
83
+ if bucket is None:
84
+ buckets[hk] = (tup, [r])
85
+ order.append(hk)
86
+ else:
87
+ bucket[1].append(r)
88
+ order.sort()
89
+
90
+ result = []
91
+ for hk in order:
92
+ tup, grp = buckets[hk]
93
+ out = {k: tup[i] for i, k in enumerate(index_keys)}
94
+ for key, cv_key in value_cols:
95
+ cell = [r.get(values) for r in grp if _safe_key(r.get(columns)) == cv_key]
96
+ out[key] = aggregate(agg, cell) if cell else fill
97
+ if totals:
98
+ out[TOTAL_KEY] = aggregate(agg, [r.get(values) for r in grp])
99
+ result.append(out)
100
+ return col_defs, result
@@ -0,0 +1,98 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """Data providers for DataTable Pro virtualization.
6
+
7
+ A DataProvider supplies rows to the virtualized model in windows, so the model
8
+ never holds more than what has been scrolled to. Sort and filter are pushed
9
+ DOWN to the provider - it returns rows already sorted/filtered from the source
10
+ (DB, API, ...) - so ordering and filtering are correct across the whole dataset,
11
+ not just the loaded window.
12
+
13
+ sort : (column_key, ascending) tuple, or None
14
+ filter : free-text string, or None
15
+ """
16
+ import inspect
17
+
18
+
19
+ class DataProvider(object):
20
+ """Abstract row source. Subclass and implement ``fetch``; override
21
+ ``totalRowCount`` when the (filtered) total is known. Return None from
22
+ ``totalRowCount`` for streaming/unknown sources (fetch is then called until
23
+ it returns a short/empty window)."""
24
+
25
+ def totalRowCount(self, filter=None):
26
+ return None
27
+
28
+ def fetch(self, offset, limit, sort=None, filter=None):
29
+ """Return up to ``limit`` row dicts starting at ``offset`` of the
30
+ sorted+filtered result. Return a short/empty list at the end."""
31
+ raise NotImplementedError
32
+
33
+
34
+ class ListDataProvider(DataProvider):
35
+ """In-memory provider with full sort/filter push-down. A working reference
36
+ (and handy for tests / feeding a large materialised dataset lazily). The
37
+ sorted+filtered view is memoised per (sort, filter)."""
38
+
39
+ def __init__(self, rows):
40
+ self._rows = list(rows or [])
41
+ self._cache_key = object() # sentinel: nothing cached yet
42
+ self._cache = None
43
+
44
+ @staticmethod
45
+ def _matches(row, needle):
46
+ return any(needle in str(v).lower() for v in row.values())
47
+
48
+ def _resolved(self, sort, filter):
49
+ key = (sort, filter)
50
+ if key == self._cache_key:
51
+ return self._cache
52
+ if filter:
53
+ needle = str(filter).lower()
54
+ rows = [r for r in self._rows if self._matches(r, needle)]
55
+ else:
56
+ rows = list(self._rows)
57
+ if sort:
58
+ col, ascending = sort
59
+ rows.sort(key=lambda r: (r.get(col) is None, r.get(col)),
60
+ reverse=not ascending)
61
+ self._cache_key = key
62
+ self._cache = rows
63
+ return rows
64
+
65
+ def totalRowCount(self, filter=None):
66
+ return len(self._resolved(None, filter))
67
+
68
+ def fetch(self, offset, limit, sort=None, filter=None):
69
+ return self._resolved(sort, filter)[offset:offset + limit]
70
+
71
+
72
+ class CallableDataProvider(DataProvider):
73
+ """Adapt plain callables. ``fetch_fn`` may accept ``(offset, limit)`` or
74
+ ``(offset, limit, sort, filter)`` - the arity is detected. ``total`` is an
75
+ int, a zero-arg callable, or None for streaming."""
76
+
77
+ def __init__(self, fetch_fn, total=None):
78
+ self._fetch = fetch_fn
79
+ self._total = total
80
+ try:
81
+ params = list(inspect.signature(fetch_fn).parameters.values())
82
+ if any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params):
83
+ self._arity = 4
84
+ else:
85
+ self._arity = sum(
86
+ 1 for p in params
87
+ if p.kind in (inspect.Parameter.POSITIONAL_ONLY,
88
+ inspect.Parameter.POSITIONAL_OR_KEYWORD))
89
+ except (ValueError, TypeError):
90
+ self._arity = 2
91
+
92
+ def totalRowCount(self, filter=None):
93
+ return self._total() if callable(self._total) else self._total
94
+
95
+ def fetch(self, offset, limit, sort=None, filter=None):
96
+ if self._arity >= 4:
97
+ return list(self._fetch(offset, limit, sort, filter) or [])
98
+ return list(self._fetch(offset, limit) or [])