visidata 2.11.1__py3-none-any.whl → 3.0.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.
Files changed (256) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +259 -42
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +21 -3
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +78 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. {vgit → visidata/apps/vgit}/blame.py +5 -2
  18. {vgit → visidata/apps/vgit}/branch.py +31 -16
  19. {vgit → visidata/apps/vgit}/config.py +3 -3
  20. visidata/apps/vgit/diff.py +169 -0
  21. visidata/apps/vgit/gitsheet.py +161 -0
  22. {vgit → visidata/apps/vgit}/grep.py +6 -5
  23. visidata/apps/vgit/log.py +81 -0
  24. {vgit → visidata/apps/vgit}/main.py +18 -5
  25. {vgit → visidata/apps/vgit}/remote.py +8 -4
  26. visidata/apps/vgit/repos.py +71 -0
  27. {vgit → visidata/apps/vgit}/setup.py +6 -4
  28. visidata/apps/vgit/stash.py +69 -0
  29. visidata/apps/vgit/status.py +204 -0
  30. {vgit → visidata/apps/vgit}/statusbar.py +2 -0
  31. visidata/basesheet.py +63 -51
  32. visidata/canvas.py +208 -93
  33. visidata/choose.py +6 -6
  34. visidata/clean_names.py +29 -0
  35. visidata/clipboard.py +73 -17
  36. visidata/cliptext.py +220 -46
  37. visidata/cmdlog.py +88 -114
  38. visidata/color.py +142 -56
  39. visidata/column.py +121 -129
  40. visidata/ddw/input.ddw +74 -79
  41. visidata/ddw/regex.ddw +57 -0
  42. visidata/ddwplay.py +33 -14
  43. visidata/deprecated.py +77 -3
  44. visidata/desktop/visidata.desktop +7 -0
  45. visidata/editor.py +12 -6
  46. visidata/errors.py +6 -2
  47. visidata/experimental/__init__.py +0 -0
  48. visidata/experimental/diff_sheet.py +29 -0
  49. visidata/experimental/digit_autoedit.py +6 -0
  50. visidata/experimental/gdrive.py +89 -0
  51. visidata/experimental/google.py +37 -0
  52. visidata/experimental/gsheets.py +79 -0
  53. visidata/experimental/live_search.py +37 -0
  54. visidata/experimental/liveupdate.py +45 -0
  55. visidata/experimental/mark.py +133 -0
  56. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  57. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  58. visidata/experimental/rownum.py +73 -0
  59. visidata/experimental/slide_cells.py +26 -0
  60. visidata/expr.py +8 -4
  61. visidata/extensible.py +22 -4
  62. visidata/features/__init__.py +0 -0
  63. visidata/features/addcol_audiometadata.py +42 -0
  64. visidata/features/addcol_histogram.py +34 -0
  65. visidata/features/canvas_save_svg.py +69 -0
  66. visidata/features/change_precision.py +46 -0
  67. visidata/features/cmdpalette.py +197 -0
  68. visidata/features/colorbrewer.py +363 -0
  69. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  70. visidata/features/command_server.py +105 -0
  71. visidata/features/currency_to_usd.py +70 -0
  72. visidata/{customdate.py → features/customdate.py} +2 -0
  73. visidata/features/dedupe.py +132 -0
  74. visidata/{describe.py → features/describe.py} +17 -15
  75. visidata/features/errors_guide.py +26 -0
  76. visidata/features/expand_cols.py +202 -0
  77. visidata/{fill.py → features/fill.py} +3 -1
  78. visidata/{freeze.py → features/freeze.py} +11 -6
  79. visidata/features/graph_seaborn.py +79 -0
  80. visidata/features/helloworld.py +10 -0
  81. visidata/features/hint_types.py +17 -0
  82. visidata/{incr.py → features/incr.py} +5 -0
  83. visidata/{join.py → features/join.py} +107 -53
  84. visidata/features/known_cols.py +21 -0
  85. visidata/features/layout.py +62 -0
  86. visidata/{melt.py → features/melt.py} +32 -21
  87. visidata/features/normcol.py +118 -0
  88. visidata/features/open_config.py +7 -0
  89. visidata/features/open_syspaste.py +18 -0
  90. visidata/features/ping.py +157 -0
  91. visidata/features/procmgr.py +208 -0
  92. visidata/features/random_sample.py +6 -0
  93. visidata/{regex.py → features/regex.py} +47 -31
  94. visidata/features/reload_every.py +55 -0
  95. visidata/features/rename_col_cascade.py +30 -0
  96. visidata/features/scroll_context.py +60 -0
  97. visidata/features/select_equal_selected.py +11 -0
  98. visidata/features/setcol_fake.py +65 -0
  99. visidata/{slide.py → features/slide.py} +77 -21
  100. visidata/features/sparkline.py +48 -0
  101. visidata/features/status_source.py +20 -0
  102. visidata/{sysedit.py → features/sysedit.py} +2 -1
  103. visidata/features/sysopen_mailcap.py +46 -0
  104. visidata/features/term_extras.py +13 -0
  105. visidata/{transpose.py → features/transpose.py} +5 -4
  106. visidata/features/type_ipaddr.py +73 -0
  107. visidata/features/type_url.py +11 -0
  108. visidata/{unfurl.py → features/unfurl.py} +9 -9
  109. visidata/{window.py → features/window.py} +2 -2
  110. visidata/form.py +50 -21
  111. visidata/freqtbl.py +81 -33
  112. visidata/fuzzymatch.py +414 -0
  113. visidata/graph.py +105 -33
  114. visidata/guide.py +200 -0
  115. visidata/help.py +75 -44
  116. visidata/hint.py +39 -0
  117. visidata/indexsheet.py +109 -0
  118. visidata/input_history.py +55 -0
  119. visidata/interface.py +58 -0
  120. visidata/keys.py +20 -16
  121. visidata/loaders/__init__.py +9 -0
  122. visidata/loaders/_pandas.py +61 -21
  123. visidata/loaders/api_airtable.py +70 -0
  124. visidata/loaders/api_bitio.py +102 -0
  125. visidata/loaders/api_matrix.py +148 -0
  126. visidata/loaders/api_reddit.py +306 -0
  127. visidata/loaders/api_zulip.py +249 -0
  128. visidata/loaders/archive.py +41 -7
  129. visidata/loaders/arrow.py +7 -7
  130. visidata/loaders/conll.py +49 -0
  131. visidata/loaders/csv.py +25 -7
  132. visidata/loaders/eml.py +3 -4
  133. visidata/loaders/f5log.py +1204 -0
  134. visidata/loaders/fec.py +325 -0
  135. visidata/loaders/fixed_width.py +2 -4
  136. visidata/loaders/frictionless.py +3 -3
  137. visidata/loaders/geojson.py +8 -5
  138. visidata/loaders/google.py +48 -0
  139. visidata/loaders/graphviz.py +4 -4
  140. visidata/loaders/hdf5.py +4 -4
  141. visidata/loaders/html.py +54 -12
  142. visidata/loaders/http.py +84 -30
  143. visidata/loaders/imap.py +20 -10
  144. visidata/loaders/jrnl.py +52 -0
  145. visidata/loaders/json.py +83 -29
  146. visidata/loaders/jsonla.py +74 -0
  147. visidata/loaders/lsv.py +15 -11
  148. visidata/loaders/mailbox.py +40 -0
  149. visidata/loaders/markdown.py +1 -3
  150. visidata/loaders/mbtiles.py +4 -5
  151. visidata/loaders/mysql.py +11 -13
  152. visidata/loaders/npy.py +7 -7
  153. visidata/loaders/odf.py +4 -1
  154. visidata/loaders/orgmode.py +428 -0
  155. visidata/loaders/pandas_freqtbl.py +14 -20
  156. visidata/loaders/parquet.py +62 -6
  157. visidata/loaders/pcap.py +3 -3
  158. visidata/loaders/pdf.py +4 -3
  159. visidata/loaders/png.py +19 -13
  160. visidata/loaders/postgres.py +9 -8
  161. visidata/loaders/rec.py +7 -3
  162. visidata/loaders/s3.py +342 -0
  163. visidata/loaders/sas.py +5 -5
  164. visidata/loaders/scrape.py +186 -0
  165. visidata/loaders/shp.py +6 -5
  166. visidata/loaders/spss.py +5 -6
  167. visidata/loaders/sqlite.py +68 -28
  168. visidata/loaders/texttables.py +1 -1
  169. visidata/loaders/toml.py +60 -0
  170. visidata/loaders/tsv.py +61 -19
  171. visidata/loaders/ttf.py +19 -7
  172. visidata/loaders/unzip_http.py +6 -5
  173. visidata/loaders/usv.py +1 -1
  174. visidata/loaders/vcf.py +16 -16
  175. visidata/loaders/vds.py +10 -7
  176. visidata/loaders/vdx.py +30 -5
  177. visidata/loaders/xlsb.py +8 -1
  178. visidata/loaders/xlsx.py +145 -25
  179. visidata/loaders/xml.py +6 -3
  180. visidata/loaders/xword.py +4 -4
  181. visidata/loaders/yaml.py +15 -5
  182. visidata/macros.py +129 -42
  183. visidata/main.py +119 -94
  184. visidata/mainloop.py +101 -155
  185. visidata/man/parse_options.py +2 -2
  186. visidata/man/vd.1 +302 -149
  187. visidata/man/vd.txt +291 -154
  188. visidata/memory.py +3 -3
  189. visidata/menu.py +104 -423
  190. visidata/metasheets.py +59 -141
  191. visidata/modify.py +78 -23
  192. visidata/motd.py +3 -3
  193. visidata/mouse.py +137 -0
  194. visidata/movement.py +43 -35
  195. visidata/optionssheet.py +99 -0
  196. visidata/path.py +113 -32
  197. visidata/pivot.py +73 -47
  198. visidata/plugins.py +65 -192
  199. visidata/pyobj.py +55 -205
  200. visidata/rename_col.py +20 -0
  201. visidata/save.py +37 -20
  202. visidata/search.py +54 -10
  203. visidata/selection.py +84 -5
  204. visidata/settings.py +162 -25
  205. visidata/sheets.py +239 -260
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +114 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/benchmark.csv +52 -0
  213. visidata/tests/conftest.py +3 -3
  214. visidata/tests/test_cliptext.py +39 -0
  215. visidata/tests/test_commands.py +65 -7
  216. visidata/tests/test_edittext.py +2 -2
  217. visidata/tests/test_features.py +28 -0
  218. visidata/tests/test_menu.py +14 -0
  219. visidata/tests/test_path.py +13 -4
  220. visidata/text_source.py +53 -0
  221. visidata/textsheet.py +10 -3
  222. visidata/theme.py +44 -0
  223. visidata/themes/__init__.py +0 -0
  224. visidata/themes/ascii8.py +84 -0
  225. visidata/themes/asciimono.py +84 -0
  226. visidata/themes/light.py +17 -0
  227. visidata/threads.py +89 -40
  228. visidata/tuiwin.py +22 -0
  229. visidata/type_currency.py +22 -3
  230. visidata/type_date.py +31 -9
  231. visidata/type_floatsi.py +5 -1
  232. visidata/undo.py +17 -5
  233. visidata/utils.py +106 -23
  234. visidata/vdobj.py +28 -17
  235. visidata/windows.py +10 -0
  236. visidata/wrappers.py +9 -3
  237. visidata-3.0.1.data/data/share/applications/visidata.desktop +7 -0
  238. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/vd.1 +302 -149
  239. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/visidata.1 +302 -149
  240. visidata-3.0.1.data/scripts/vd2to3.vdx +9 -0
  241. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/METADATA +12 -8
  242. visidata-3.0.1.dist-info/RECORD +258 -0
  243. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/WHEEL +1 -1
  244. vgit/__init__.py +0 -1
  245. vgit/gitsheet.py +0 -164
  246. visidata/layout.py +0 -44
  247. visidata/misc.py +0 -5
  248. visidata-2.11.1.data/scripts/vgit +0 -9
  249. visidata-2.11.1.dist-info/RECORD +0 -155
  250. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  251. {vgit → visidata/apps/vgit}/abort.py +0 -0
  252. /visidata/{repeat.py → features/repeat.py} +0 -0
  253. {visidata-2.11.1.data → visidata-3.0.1.data}/scripts/vd +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/LICENSE.gpl3 +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/entry_points.txt +0 -0
  256. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,748 @@
1
+ from copy import copy
2
+ import functools
3
+ import operator
4
+ import re
5
+
6
+ from contextlib import contextmanager
7
+ from visidata import VisiData, Sheet, IndexSheet, vd, date, anytype, vlen, clipdraw, colors, stacktrace, PyobjSheet, BaseSheet, ExpectedException
8
+ from visidata import ItemColumn, AttrColumn, Column, TextSheet, asyncthread, wrapply, ColumnsSheet, UNLOADED, ExprColumn, undoAttrCopyFunc, ENTER
9
+
10
+ vd.option('disp_ibis_sidebar', 'pending_sql', 'which sidebar property to display')
11
+ vd.option('sql_always_count', False, 'whether to include count of total number of results')
12
+ vd.option('ibis_limit', 500, 'max number of rows to get in query')
13
+
14
+
15
+ def vdtype_to_ibis_type(t):
16
+ from ibis.expr import datatypes as dt
17
+ return {
18
+ int: dt.int,
19
+ float: dt.float,
20
+ date: dt.date,
21
+ str: dt.string,
22
+ }.get(t)
23
+
24
+
25
+ def dtype_to_vdtype(dtype):
26
+ from ibis.expr import datatypes as dt
27
+
28
+ try:
29
+ if isinstance(dtype, dt.Decimal):
30
+ if dtype.scale == 0:
31
+ return int
32
+ else:
33
+ return float
34
+ if isinstance(dtype, dt.Integer):
35
+ return int
36
+ if isinstance(dtype, dt.Floating):
37
+ return float
38
+ if isinstance(dtype, (dt.Date, dt.Timestamp)):
39
+ return date
40
+ except TypeError:
41
+ # For categoricals and other pandas-defined dtypes
42
+ pass
43
+ return anytype
44
+
45
+
46
+ @VisiData.api
47
+ def configure_ibis(vd):
48
+ import ibis
49
+
50
+ vd.aggregator('collect', ibis.expr.types.AnyValue.collect, 'collect a list of values')
51
+ ibis.options.verbose_log = vd.status
52
+ if vd.options.debug:
53
+ ibis.options.verbose = True
54
+
55
+ if 'ibis_type' not in set(c.expr for c in ColumnsSheet.columns):
56
+ ColumnsSheet.columns += [
57
+ AttrColumn('ibis_type', type=str)
58
+ ]
59
+
60
+
61
+ @VisiData.api
62
+ def open_vdsql(vd, p, filetype=None):
63
+ vd.configure_ibis()
64
+
65
+ # on-demand aliasing, so we don't need deps for all backends
66
+ ext_aliases = dict(db='sqlite', ddb='duckdb', sqlite3='sqlite')
67
+ if p.ext in ext_aliases:
68
+ setattr(ibis, p.ext, ext_aliases.get(p.ext))
69
+
70
+ return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
71
+ ibis_conpool=IbisConnectionPool(p), sheet_type=IbisTableSheet)
72
+
73
+ vd.open_ibis = vd.open_vdsql
74
+
75
+
76
+ class IbisConnectionPool:
77
+ def __init__(self, source, pool=None, total=0):
78
+ self.source = source
79
+ self.pool = pool if pool is not None else []
80
+ self.total = total
81
+
82
+ def __copy__(self):
83
+ return IbisConnectionPool(self.source, pool=self.pool, total=self.total)
84
+
85
+ @contextmanager
86
+ def get_conn(self):
87
+ if not self.pool:
88
+ import sqlalchemy
89
+ try:
90
+ import ibis
91
+ r = ibis.connect(str(self.source))
92
+ except sqlalchemy.exc.NoSuchModuleError as e:
93
+ dialect = str(e).split(':')[-1]
94
+ vd.warning(f'{dialect} not installed')
95
+ vd.fail(f'pip install ibis-framework[{dialect}]')
96
+ else:
97
+ r = self.pool.pop(0)
98
+
99
+ try:
100
+ yield r
101
+ finally:
102
+ self.pool.append(r)
103
+
104
+
105
+ class IbisTableIndexSheet(IndexSheet):
106
+ # sheet_type = IbisTableSheet # set below
107
+
108
+ @property
109
+ def con(self):
110
+ return self.ibis_conpool.get_conn()
111
+
112
+ def rawSql(self, qstr):
113
+ with self.con as con:
114
+ return IbisTableSheet('rawsql',
115
+ ibis_conpool=self.ibis_conpool,
116
+ ibis_source=qstr,
117
+ source=qstr,
118
+ query=con.sql(qstr))
119
+
120
+ def iterload(self):
121
+ with self.con as con:
122
+ if self.database_name:
123
+ con.set_database(self.database_name)
124
+
125
+ # use the actual count instead of the returned limit
126
+ nrows_col = self.column('rows')
127
+ nrows_col.expr = 'countRows'
128
+ nrows_col.width += 3
129
+
130
+ for tblname in con.list_tables():
131
+ yield self.sheet_type(tblname,
132
+ ibis_source=self.source,
133
+ ibis_filetype=self.filetype,
134
+ ibis_conpool=self.ibis_conpool,
135
+ database_name=self.database_name,
136
+ table_name=tblname,
137
+ source=self.source,
138
+ query=None)
139
+
140
+
141
+ class IbisColumn(ItemColumn):
142
+ @property
143
+ def ibis_type(self):
144
+ return self.sheet.query[self.ibis_name].type()
145
+
146
+ @asyncthread
147
+ def memo_aggregate(self, agg, rows):
148
+ 'Show aggregated value in status, and add ibis expr to memory for use later.'
149
+
150
+ aggexpr = self.ibis_aggr(agg.name) # ignore rows, do over whole query
151
+
152
+ with self.sheet.con as con:
153
+ aggval = con.execute(aggexpr)
154
+
155
+ typedval = wrapply(agg.type or self.type, aggval)
156
+ dispval = self.format(typedval)
157
+ k = self.name+'_'+agg.name
158
+ vd.status(f'{k}={dispval}')
159
+ vd.memory[k] = aggval
160
+ # store aggexpr somewhere to use in later subquery
161
+
162
+ def expand(self, rows):
163
+ return self.expand_struct(rows)
164
+
165
+ def expand_struct(self, rows):
166
+ oldexpr = self.sheet.ibis_current_expr
167
+ struct_field = self.get_ibis_col(oldexpr)
168
+ # if struct_field is not StructType:
169
+ # vd.fail('vdsql can only expand Struct columns')
170
+
171
+ struct_fields = [struct_field[name] for name in struct_field.names]
172
+ expandedCols = super().expand(rows) # this must go after ibis_current_expr, because it alters ibis_current_expr
173
+ fields = []
174
+ for ibiscol, expcol in zip(struct_fields, expandedCols):
175
+ fields.append(ibiscol.name(expcol.name))
176
+ # self.sheet.query = oldexpr.drop([struct_field.get_name()]).mutate(fields)
177
+ self.sheet.query = oldexpr.mutate(fields)
178
+ return expandedCols
179
+
180
+
181
+ class LazyIbisColMap:
182
+ def __init__(self, sheet, q):
183
+ self._sheet = sheet
184
+ self._query = q
185
+ self._map = {col.name: col for col in sheet.columns}
186
+
187
+ def __getitem__(self, k):
188
+ col = self._map[k]
189
+ return col.get_ibis_col(self._query)
190
+
191
+
192
+ class IbisTableSheet(Sheet):
193
+ @property
194
+ def con(self):
195
+ return self.ibis_conpool.get_conn()
196
+
197
+ def choose_sidebar(self):
198
+ sidebars = ['base_sql', 'pending_sql', 'ibis_current_expr', 'curcol_sql', 'pending_expr']
199
+ opts = []
200
+ for s in sidebars:
201
+ try:
202
+ opts.append({'key': s, 'value':getattr(self, s)})
203
+ except Exception as e:
204
+ if self.options.debug:
205
+ vd.exceptionCaught()
206
+
207
+ vd.options.disp_ibis_sidebar = vd.chooseOne(opts)
208
+
209
+ @property
210
+ def curcol_sql(self):
211
+ expr = self.cursorCol.get_ibis_col(self.ibis_current_expr)
212
+ if expr is not None:
213
+ return self.ibis_to_sql(expr, fragment=True)
214
+
215
+ def ibis_to_sql(self, expr, fragment=False):
216
+ import sqlparse
217
+ with self.con as con:
218
+ context = con.compiler.make_context()
219
+ trclass = con.compiler.translator_class(expr.op(), context=context)
220
+ if fragment:
221
+ compiled = trclass.get_result()
222
+ else:
223
+ compiled = con.compile(expr)
224
+ if not isinstance(compiled, str):
225
+ compiled = str(compiled.compile(compile_kwargs={'literal_binds': True}))
226
+ return sqlparse.format(compiled, reindent=True, keyword_case='upper', wrap_after=40)
227
+
228
+ @property
229
+ def sidebar(self) -> str:
230
+ sbtype = self.options.disp_ibis_sidebar
231
+ if sbtype:
232
+ txt = str(getattr(self, sbtype, ''))
233
+ if txt:
234
+ return f'# {sbtype}\n'+txt
235
+
236
+ @property
237
+ def ibis_locals(self):
238
+ return LazyIbisColMap(self, self.query)
239
+
240
+ def select_row(self, row):
241
+ k = self.rowkey(row) or vd.fail('need key column to select individual rows')
242
+ super().selectRow(row)
243
+ self.ibis_selection.append(self.matchRowKeyExpr(row))
244
+
245
+ def stoggle_row(self, row):
246
+ vd.fail('cannot toggle selection of individual row in vdsql')
247
+
248
+ def unselect_row(self, row):
249
+ super().unselectRow(row)
250
+ self.ibis_selection = [ self.ibis_filter & ~self.matchRowKeyExpr(row) ]
251
+
252
+ def matchRowKeyExpr(self, row):
253
+ import ibis
254
+ k = self.rowkey(row) or vd.fail('need key column to select individual rows')
255
+
256
+ return functools.reduce(operator.and_, [
257
+ c.get_ibis_col(self.query, typed=True) == k[i]
258
+ for i, c in enumerate(self.keyCols)
259
+ ])
260
+
261
+ @property
262
+ def ibis_current_expr(self):
263
+ return self.get_current_expr(typed=False)
264
+
265
+ def get_current_expr(self, typed=False):
266
+ q = self.query
267
+ extra_cols = {}
268
+ for c in self.visibleCols:
269
+ ibis_col = c.get_ibis_col(q, typed=typed)
270
+ if ibis_col is not None:
271
+ extra_cols[c.name] = ibis_col
272
+ else:
273
+ vd.warning(f'no column {c.name}')
274
+
275
+ if extra_cols:
276
+ q = q.mutate(**extra_cols)
277
+
278
+ return q
279
+
280
+ @property
281
+ def ibis_filter(self):
282
+ import ibis
283
+ selectors = [self.ibisCompileExpr(f, self.get_current_expr(typed=True)) for f in self.ibis_selection]
284
+ if not selectors:
285
+ return ibis.literal(True)
286
+ return functools.reduce(operator.or_, selectors)
287
+
288
+ @property
289
+ def pending_expr(self):
290
+ import ibis
291
+ q = self.get_current_expr(typed=True)
292
+ if self.ibis_selection:
293
+ q = q.filter(self.ibis_filter)
294
+
295
+ if self._ordering:
296
+ colorder = []
297
+ for col, rev in self._ordering:
298
+ ibiscol = col.get_ibis_col(q) #1856
299
+ if rev:
300
+ ibiscol = ibis.desc(ibiscol)
301
+ colorder.append(ibiscol)
302
+ q = q.order_by(colorder)
303
+
304
+ return q
305
+
306
+ def ibisCompileExpr(self, expr, q):
307
+ if isinstance(expr, str):
308
+ return eval(expr, vd.getGlobals(), LazyIbisColMap(self, q))
309
+ else:
310
+ return expr
311
+
312
+ def evalIbisExpr(self, expr):
313
+ return eval(expr, vd.getGlobals(), self.ibis_locals)
314
+
315
+ @property
316
+ def base_sql(self):
317
+ return self.sqlize(self.ibis_current_expr)
318
+
319
+ @property
320
+ def pending_sql(self):
321
+ return self.sqlize(self.pending_expr)
322
+
323
+ def sqlize(self, expr):
324
+ if vd.options.debug:
325
+ expr = self.withRowcount(expr)
326
+ return self.ibis_to_sql(expr)
327
+
328
+ @property
329
+ def substrait(self):
330
+ from ibis_substrait.compiler.core import SubstraitCompiler
331
+ compiler = SubstraitCompiler()
332
+ return compiler.compile(self.ibis_current_expr)
333
+
334
+ def withRowcount(self, q):
335
+ if self.options.sql_always_count:
336
+ # return q.mutate(__n__=q.count())
337
+ return q.cross_join(q.aggregate(__n__=lambda t: t.count()))
338
+ return q
339
+
340
+ def beforeLoad(self):
341
+ self.options.disp_rstatus_fmt = self.options.disp_rstatus_fmt.replace('nRows', 'countRows')
342
+ self.options.disp_rstatus_fmt = self.options.disp_rstatus_fmt.replace('nSelectedRows', 'countSelectedRows')
343
+
344
+ def baseQuery(self, con):
345
+ 'Return base table for {database_name}.{table_name}'
346
+ import ibis
347
+ tbl = con.table(self.table_name)
348
+ return ibis.table(tbl.schema(), name=self.fqtblname(con))
349
+
350
+ def fqtblname(self, con) -> str:
351
+ 'Return fully-qualified table name including database/schema, or whatever connection needs to identify this table.'
352
+ if hasattr(con, '_fully_qualified_name'):
353
+ return con._fully_qualified_name(self.table_name, self.database_name)
354
+ return self.table_name
355
+
356
+ def iterload(self):
357
+ with self.con as con:
358
+ if self.query is None:
359
+ self.query = self.baseQuery(con)
360
+
361
+ self.reloadColumns(self.query) # columns based on query without metadata
362
+ self.query_result = con.execute(self.withRowcount(self.query),
363
+ limit=self.options.ibis_limit or None)
364
+
365
+ yield from self.query_result.itertuples()
366
+
367
+
368
+ def reloadColumns(self, expr, start=1):
369
+ oldkeycols = {c.name:c for c in self.keyCols}
370
+ self._nrows_col = -1
371
+
372
+ for i, (colname, dtype) in enumerate(expr.schema().items(), start=start):
373
+ keycol=oldkeycols.get(colname, Column()).keycol
374
+ if i-start < self.nKeys:
375
+ keycol = i+1
376
+
377
+ if colname == '__n__':
378
+ self._nrows_col = i
379
+ continue
380
+
381
+ self.addColumn(IbisColumn(colname, i,
382
+ type=dtype_to_vdtype(dtype),
383
+ keycol=keycol,
384
+ ibis_name=colname))
385
+
386
+ @property
387
+ def countSelectedRows(self):
388
+ return f'{self.nSelectedRows}+'
389
+
390
+ @property
391
+ def countRows(self):
392
+ if self.rows is UNLOADED:
393
+ return None
394
+ if not self.rows or self._nrows_col < 0:
395
+ return self.nRows
396
+ return self.rows[0][self._nrows_col] # __n__
397
+
398
+ def groupBy(self, groupByCols):
399
+ from ibis import _
400
+ import ibis
401
+ from ibis.expr import datatypes as dt
402
+ aggr_cols = [groupByCols[0].ibis_col.count().name('count')]
403
+ for c in self.visibleCols:
404
+ aggr_cols.extend(c.ibis_aggrs)
405
+
406
+ q = self.ibis_current_expr
407
+ groupq = q.aggregate(aggr_cols, by=[c.ibis_col for c in groupByCols])
408
+ try:
409
+ win = ibis.window(order_by=ibis.NA)
410
+ except ibis.common.exceptions.IbisTypeError: # ibis bug: there is not yet a good workaround that covers all backends
411
+ win = ibis.window(order_by=None)
412
+ groupq = groupq.mutate(percent=_['count']*100 / _['count'].sum().over(win))
413
+
414
+ histolen = 40
415
+ histogram_char = self.options.disp_histogram
416
+ if histogram_char and len(aggr_cols) == 1:
417
+ groupq = groupq.mutate(maxcount=_['count'].max())
418
+ hval = ibis.literal(histogram_char, type=dt.string)
419
+
420
+ def _histogram(t):
421
+ return hval.repeat((histolen*t['count']/t.maxcount).cast(dt.int))
422
+
423
+ groupq = groupq.mutate(histogram=_histogram)
424
+
425
+ groupq = groupq.order_by(ibis.desc('count'))
426
+
427
+ return IbisFreqTable(self.name, *(col.name for col in groupByCols), 'freq',
428
+ ibis_conpool=self.ibis_conpool,
429
+ ibis_source=self.ibis_source,
430
+ source=self,
431
+ groupByCols=groupByCols,
432
+ query=groupq,
433
+ nKeys=len(groupByCols))
434
+
435
+ def unfurl_col(self, col):
436
+ vs = copy(self)
437
+ vs.names = [self.name, col.name, 'unfurled']
438
+ vs.query = self.ibis_current_expr.mutate(**{col.name:col.ibis_col.unnest()})
439
+ vs.cursorVisibleColIndex = self.cursorVisibleColIndex
440
+ return vs
441
+
442
+ def openJoin(self, others, jointype=''):
443
+ sheets = [self] + others
444
+
445
+ sheets[1:] or vd.fail("join requires more than 1 sheet")
446
+
447
+ if jointype == 'append':
448
+ q = self.ibis_current_expr
449
+ for other in others:
450
+ q = q.union(other.ibis_current_expr)
451
+ return IbisTableSheet('&'.join(vs.name for vs in sheets), query=q, ibis_source=self.ibis_source, ibis_conpool=self.ibis_conpool)
452
+
453
+ for s in sheets:
454
+ s.keyCols or vd.fail(f'{s.name} has no key cols to join')
455
+
456
+ if jointype in ['extend', 'outer']:
457
+ jointype = 'left'
458
+ elif jointype in ['full']:
459
+ jointype = 'outer'
460
+ # elif jointype in ['inner']:
461
+ # jointype = 'inner'
462
+
463
+
464
+ q = self.ibis_current_expr
465
+ for other in others:
466
+ preds = [(a.ibis_col == b.ibis_col) for a, b in zip(self.keyCols, other.keyCols)]
467
+ q = q.join(other.ibis_current_expr, predicates=preds, how=jointype, suffixes=('', '_'+other.name))
468
+
469
+ return IbisTableSheet('+'.join(vs.name for vs in sheets), sources=sheets, query=q, ibis_source=self.ibis_source, ibis_conpool=self.ibis_conpool)
470
+
471
+
472
+ @Column.property
473
+ def ibis_col(col):
474
+ return col.get_ibis_col(col.sheet.ibis_current_expr)
475
+
476
+
477
+ @Column.api
478
+ def get_ibis_col(col, query:'ibis.Expr', typed=False) -> 'ibis.Expr':
479
+ 'Return ibis.Expr for `col` within context of `query`, cast by VisiData column type if `typed`.'
480
+ import ibis.common.exceptions
481
+
482
+ r = None
483
+ if isinstance(col, ExprColumn):
484
+ r = col.sheet.evalIbisExpr(col.expr)
485
+ elif isinstance(col, vd.ExpandedColumn):
486
+ r = query[col.name]
487
+ elif not hasattr(col, 'ibis_name'):
488
+ return
489
+ else:
490
+ try:
491
+ r = query[col.ibis_name]
492
+ except (ibis.common.exceptions.IbisTypeError, AttributeError):
493
+ r = query[col.name]
494
+
495
+ if r is None:
496
+ return r
497
+
498
+ if typed:
499
+ import ibis.expr.datatypes as dt
500
+ if col.type is str: r = r.cast(dt.string)
501
+ if col.type is int: r = r.cast(dt.int)
502
+ if col.type is float: r = r.cast(dt.float)
503
+ if col.type is date:
504
+ if not isinstance(r.type(), (dt.Timestamp, dt.Date)):
505
+ r = r.cast(dt.date)
506
+
507
+ r = r.name(col.name)
508
+ return r
509
+
510
+
511
+ @Column.property
512
+ def ibis_aggrs(col):
513
+ return [col.ibis_aggr(aggname) for aggname in (col.aggstr or '').split()]
514
+
515
+
516
+ @Column.api
517
+ def ibis_aggr(col, aggname):
518
+ aggname = {
519
+ 'avg': 'mean',
520
+ 'median': 'approx_median',
521
+ 'mode': 'notimpl',
522
+ 'distinct': 'nunique',
523
+ 'list': 'collect',
524
+ 'stdev': 'std',
525
+ # 'p99': 'quantile(0.99)',
526
+ # 'q10': 'quantile([.1,.2,.3,.4,.5,.6,.7,.8,.9])',
527
+ }.get(aggname, aggname)
528
+
529
+ agg = getattr(col.ibis_col, aggname)
530
+ return agg().name(f'{aggname}_{col.name}')
531
+
532
+
533
+ IbisTableSheet.init('ibis_selection', list, copy=False)
534
+ IbisTableSheet.init('_sqlscr', lambda: None, copy=False)
535
+ IbisTableSheet.init('query_result', lambda: None, copy=False)
536
+ IbisTableSheet.init('ibis_conpool', lambda: None, copy=True)
537
+
538
+ @IbisTableSheet.api
539
+ def stoggle_rows(sheet):
540
+ sheet.toggle(sheet.rows)
541
+ sheet.ibis_selection = [~sheet.ibis_filter]
542
+
543
+
544
+ @IbisTableSheet.api
545
+ def clearSelected(sheet):
546
+ super(IbisTableSheet, sheet).clearSelected()
547
+ sheet.ibis_selection.clear()
548
+
549
+
550
+
551
+ @IbisTableSheet.api
552
+ def addUndoSelection(sheet):
553
+ super(IbisTableSheet, sheet).addUndoSelection()
554
+ vd.addUndo(undoAttrCopyFunc([sheet], 'ibis_selection'))
555
+
556
+
557
+ @IbisTableSheet.api
558
+ def select_equal_cell(sheet, col, typedval):
559
+ if sheet.isNullFunc()(typedval):
560
+ expr = col.ibis_col.isnull()
561
+ else:
562
+ q = sheet.get_current_expr(typed=True)
563
+ ibis_col = col.get_ibis_col(q, typed=True)
564
+ expr = (ibis_col == typedval)
565
+
566
+ sheet.ibis_selection.append(expr)
567
+ sheet.select(sheet.gatherBy(lambda r,c=col,v=typedval: c.getTypedValue(r) == v), progress=False)
568
+
569
+
570
+ @IbisTableSheet.api
571
+ def select_col_regex(sheet, col, regex):
572
+ sheet.selectByIdx(vd.searchRegex(sheet, regex=regex, columns="cursorCol"))
573
+ sheet.ibis_selection.append(col.get_ibis_col(col.sheet.query).re_search(regex))
574
+
575
+
576
+ @IbisTableSheet.api
577
+ def select_expr(sheet, expr):
578
+ sheet.select(sheet.gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)
579
+ sheet.ibis_selection.append(expr)
580
+
581
+
582
+ @IbisTableSheet.api
583
+ def addcol_split(sheet, col, delim):
584
+ from ibis.expr import datatypes as dt
585
+ c = Column(col.name+'_split',
586
+ getter=lambda col,row: col.origCol.getDisplayValue(row).split(col.expr),
587
+ expr=delim,
588
+ origCol=col,
589
+ ibis_name=col.name+'_split')
590
+ sheet.query = sheet.query.mutate(**{c.name:col.get_ibis_col(sheet.query).cast(dt.string).split(delim)})
591
+ return c
592
+
593
+
594
+ @IbisTableSheet.api
595
+ def addcol_subst(sheet, col, before='', after=''):
596
+ c = Column(col.name + "_re",
597
+ getter=lambda col,row,before=before,after=after: re.sub(before, after, col.origCol.getDisplayValue(row)),
598
+ origCol=col,
599
+ ibis_name=col.name + "_re")
600
+ sheet.query = sheet.query.mutate(**{c.name:col.get_ibis_col(sheet.query).re_replace(before, after)})
601
+ return c
602
+
603
+
604
+ @IbisTableSheet.api
605
+ def addcol_cast(sheet, col):
606
+ # sheet.query and sheet.ibis_current_expr don't match
607
+ new_type = vdtype_to_ibis_type(col.type)
608
+ if new_type is None:
609
+ vd.warning(f"no type for vd type {col.type}")
610
+ return
611
+ expr = sheet.query[col.name].cast(new_type)
612
+ sheet.query = sheet.query.mutate(**{col.name: expr})
613
+ newcol = copy(col)
614
+ col.hide()
615
+ sheet.addColumnAtCursor(newcol)
616
+
617
+ # disable not implemented commands
618
+
619
+ @BaseSheet.api
620
+ def notimpl(sheet):
621
+ vd.fail(f"{vd.activeCommand.longname} not implemented for {type(sheet).__name__}; copy to new non-ibis sheet with g'")
622
+
623
+
624
+ dml_cmds = '''addcol-bulk addcol-new add-row add-rows
625
+ copy-cell copy-cells copy-row copy-selected commit-sheet cut-cell cut-cells cut-row cut-selected delete-cell delete-cells delete-row delete-selected
626
+ edit-cell paste-after paste-before paste-cell setcell-expr
627
+ setcol-clipboard setcol-expr setcol-fake setcol-fill setcol-format-enum setcol-formatter setcol-incr setcol-incr-step setcol-input setcol-iter setcol-subst setcol-subst-all
628
+ '''.split()
629
+
630
+ neverimpl_cmds = '''
631
+ select-after select-around-n select-before select-equal-row select-error stoggle-after stoggle-before stoggle-row unselect-after unselect-before select-cols-regex unselect-cols-regex transpose
632
+ '''.split()
633
+
634
+ notimpl_cmds = '''
635
+ addcol-capture addcol-incr addcol-incr-step addcol-window
636
+ contract-col expand-col-depth expand-cols expand-cols-depth melt melt-regex pivot random-rows
637
+ select-error-col select-exact-cell select-exact-row select-rows
638
+ describe-sheet freq-summary
639
+ cache-col cache-cols
640
+ dive-selected-cells
641
+ dup-rows dup-rows-deep dup-selected-deep
642
+ '''.split()
643
+
644
+ for longname in list(notimpl_cmds) + list(neverimpl_cmds) + list(dml_cmds):
645
+ if longname:
646
+ IbisTableSheet.addCommand('', longname, 'notimpl()')
647
+
648
+ @IbisTableSheet.api
649
+ def dup_selected(sheet):
650
+ vs=copy(sheet)
651
+ vs.query=sheet.pending_expr
652
+ vs.incrementName()
653
+ vd.push(vs)
654
+
655
+
656
+ @BaseSheet.api
657
+ def incrementName(sheet):
658
+ if isinstance(sheet.names[-1], int):
659
+ sheet.names[-1] += 1
660
+ else:
661
+ sheet.names = list(sheet.names) + [1]
662
+ sheet.name = '_'.join(map(str, sheet.names))
663
+
664
+
665
+ @IbisTableSheet.api
666
+ def dup_limit(sheet, limit:int):
667
+ vs=copy(sheet)
668
+ vs.name += f"_top{limit}" if limit else "_all"
669
+ vs.query=sheet.pending_expr
670
+ vs.options.ibis_limit=limit
671
+ return vs
672
+
673
+ @IbisTableSheet.api
674
+ def rawSql(sheet, qstr):
675
+ with sheet.con as con:
676
+ return IbisTableSheet('rawsql',
677
+ ibis_conpool=sheet.ibis_conpool,
678
+ ibis_source=qstr,
679
+ source=qstr,
680
+ query=con.sql(qstr))
681
+
682
+ class IbisFreqTable(IbisTableSheet):
683
+ def freqExpr(self, row):
684
+ # matching key of grouped columns
685
+ return functools.reduce(operator.and_, [
686
+ c.get_ibis_col(self.source.query, typed=True) == self.rowkey(row)[i]
687
+ for i, c in enumerate(self.groupByCols)
688
+ ])
689
+
690
+ def selectRow(self, row):
691
+ super().selectRow(row)
692
+ self.source.select(self.gatherBy(lambda r, sheet=self, expr=self.freqExpr(row): sheet.evalExpr(expr, r)), progress=False)
693
+ self.source.ibis_selection.append(self.freqExpr(row))
694
+
695
+ def openRow(self, row):
696
+ vs = copy(self.source)
697
+ vs.names = list(vs.names) + ['_'.join(str(x) for x in self.rowkey(row))]
698
+ vs.query = self.source.query.filter(self.freqExpr(row))
699
+ return vs
700
+
701
+ def openRows(self, rows):
702
+ 'Return sheet with union of all selected items.'
703
+ vs = copy(self.source)
704
+ vs.names = list(vs.names) + ['several']
705
+
706
+ vs.query = self.source.query.filter([
707
+ functools.reduce(operator.or_, [self.freqExpr(row) for row in rows])
708
+ ])
709
+ return vs
710
+
711
+
712
+ IbisTableSheet.addCommand('F', 'freq-col', 'vd.push(groupBy([cursorCol]))')
713
+ IbisTableSheet.addCommand('gF', 'freq-keys', 'vd.push(groupBy(keyCols))')
714
+
715
+ IbisTableSheet.addCommand('"', 'dup-selected', 'vd.push(dup_selected())', 'open duplicate sheet with selected rows (default limit)')
716
+ IbisTableSheet.addCommand('z"', 'dup-limit', 'vd.push(dup_limit(input("max rows: ", value=options.ibis_limit)))', 'open duplicate sheet with only selected rows (input limit)')
717
+ IbisTableSheet.addCommand('gz"', 'dup-nolimit', 'vd.push(dup_limit(0))', 'open duplicate sheet with only selected rows (no limit--be careful!)')
718
+
719
+ IbisTableSheet.addCommand("'", 'addcol-cast', 'addcol_cast(cursorCol)')
720
+
721
+ IbisTableSheet.addCommand('zb', 'sidebar-choose', 'choose_sidebar()', 'choose vdsql sidebar to show')
722
+ IbisTableSheet.addCommand('', 'exec-sql', 'vd.push(rawSql(input("SQL query: ")))', 'open sheet with results of raw SQL query')
723
+ IbisTableSheet.addCommand('', 'addcol-subst', 'addColumnAtCursor(addcol_subst(cursorCol, **inputRegexSubstOld("transform column by regex: ")))') # deprecated
724
+ IbisTableSheet.addCommand('', 'addcol-regex-subst', 'addColumnAtCursor(addcol_subst(cursorCol, **inputRegexSubst("transform column by regex: ")))')
725
+ IbisTableSheet.addCommand('', 'addcol-split', 'addColumnAtCursor(addcol_split(cursorCol, input("split by delimiter: ", type="delim-split")))')
726
+ IbisTableSheet.addCommand('gt', 'stoggle-rows', 'stoggle_rows()', 'select rows matching current cell in current column')
727
+ IbisTableSheet.addCommand(',', 'select-equal-cell', 'select_equal_cell(cursorCol, cursorTypedValue)', 'select rows matching current cell in current column')
728
+ IbisTableSheet.addCommand('t', 'stoggle-row', 'stoggle_row(cursorRow); cursorDown(1)', 'toggle selection of current row')
729
+ IbisTableSheet.addCommand('s', 'select-row', 'select_row(cursorRow); cursorDown(1)', 'select current row')
730
+ IbisTableSheet.addCommand('u', 'unselect-row', 'unselect_row(cursorRow); cursorDown(1)', 'unselect current row')
731
+ #IbisTableSheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
732
+ #IbisTableSheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')
733
+ #IbisTableSheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
734
+
735
+ IbisTableSheet.addCommand('', 'select-col-regex', 'select_col_regex(cursorCol, inputRegex("select regex: ", type="regex", defaultLast=True))', 'select rows matching regex in current column')
736
+
737
+ IbisTableSheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select_expr(expr)', 'select rows matching Python expression in any visible column')
738
+ IbisTableSheet.addCommand('z\\', 'unselect-expr', 'expr=inputExpr("unselect by expr: "); unselect(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'unselect rows matching Python expression in any visible column')
739
+
740
+ IbisFreqTable.addCommand('g'+ENTER, 'open-selected', 'vd.push(openRows(selectedRows))')
741
+ IbisTableIndexSheet.addCommand('', 'exec-sql', 'vd.push(rawSql(input("SQL query: ")))', 'open sheet with results of raw SQL query')
742
+
743
+ IbisTableIndexSheet.class_options.load_lazy = True
744
+ IbisTableIndexSheet.sheet_type = IbisTableSheet
745
+ IbisTableSheet.class_options.clean_names = True
746
+ IbisTableSheet.class_options.regex_flags = ''
747
+
748
+ vd.addMenuItem('View', 'Sidebar', 'choose', 'sidebar-choose')