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,105 @@
1
+ import io
2
+ import json
3
+ import socket
4
+ from unittest.mock import Mock
5
+
6
+ from visidata import vd, VisiData, asyncthread, asyncignore, CommandLogRow, Sheet
7
+
8
+
9
+ vd.option('server_addr', '127.0.0.1', 'IP address to listen for commands', sheettype=None, replay=False)
10
+ vd.option('server_port', 0, 'port to listen for commands', sheettype=None, replay=False)
11
+
12
+
13
+ class SocketIO(io.RawIOBase):
14
+ def __init__(self, sock):
15
+ self.sock = sock
16
+
17
+ def read(self, sz=-1):
18
+ if (sz == -1): sz=0x7FFFFFFF
19
+ return self.sock.recv(sz)
20
+
21
+ def seekable(self):
22
+ return False
23
+
24
+
25
+ @VisiData.before
26
+ def mainloop(vd, scr):
27
+ port = vd.options.server_port
28
+ if port:
29
+ vd.command_listener(vd.options.server_addr, port)
30
+
31
+
32
+ @VisiData.api
33
+ @asyncignore
34
+ def command_listener(vd, addr, port):
35
+ while True:
36
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
37
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
38
+ s.bind((addr, port))
39
+ s.listen(1)
40
+
41
+ conn, (addr, inport) = s.accept()
42
+ vd.status(f'Connection from {addr}:{inport}')
43
+ vd.queueCommand('no-op') # update screen
44
+ vd.command_server(conn)
45
+
46
+
47
+ @VisiData.api
48
+ @asyncignore
49
+ def command_server(vd, conn):
50
+ for line in SocketIO(conn):
51
+ line = line.decode().strip()
52
+ if line == 'draw':
53
+ r = '\n'.join(json.dumps(d) for d in vd.sheet.capture_draw_object())
54
+ conn.send(r.encode('utf-8')+b'\n')
55
+ elif line.startswith('{'):
56
+ cmd = json.loads(line)
57
+ vd.queueCommand(**cmd)
58
+ else:
59
+ longname, *rest = line.split(' ', maxsplit=1)
60
+ cmd = dict(longname=longname, input=rest[0] if rest else '')
61
+ vd.queueCommand(**cmd)
62
+
63
+ conn.close()
64
+
65
+
66
+ @Sheet.api
67
+ def capture_draw_object(sheet, topRowIndex=0, nScreenRows=25):
68
+ 'capture interface at the object level'
69
+ isNull = sheet.isNullFunc()
70
+ sortkeys = {col:rev for col, rev in sheet._ordering}
71
+ rows = sheet.rows[topRowIndex:min(topRowIndex+nScreenRows+1, sheet.nRows)]
72
+
73
+ for vcolidx, col in enumerate(sheet.visibleCols):
74
+ colstate = col.__getstate__()
75
+
76
+ if col in sortkeys:
77
+ colstate['sort'] = 'desc' if sortkeys.get(col) else 'asc'
78
+
79
+ yield dict(i=vcolidx, _type='column', **colstate)
80
+
81
+ for rowidx, row in enumerate(rows):
82
+ rowstate = dict()
83
+
84
+ for notefunc in vd.rowNoters:
85
+ ch = notefunc(sheet, row)
86
+ if ch:
87
+ rowstate['note'] = rowstate.get('note', '') + ch
88
+
89
+ for vcolidx, col in enumerate(sheet.visibleCols):
90
+ cellval = col.getCell(row)
91
+
92
+ disp = ''.join(x for _, x in col.display(cellval))
93
+ cellstate = dict(display=disp)
94
+ notes = getattr(cellval, 'notes', '')
95
+ try:
96
+ if isNull and isNull(cellval.value):
97
+ notes += sheet.options.disp_note_none
98
+ except (TypeError, ValueError):
99
+ pass
100
+
101
+ if notes:
102
+ cellstate['notes'] = notes
103
+ rowstate[str(vcolidx)] = cellstate
104
+
105
+ yield dict(_type='row', i=rowidx, **rowstate)
@@ -0,0 +1,70 @@
1
+ '''Provide USD(s) function to convert string like '£300' or '205 AUD' to equivalent US$ as float.
2
+ Uses data from api.apilayer.com/fixer. Requires an API key for apilayer.com.
3
+ '''
4
+
5
+ from visidata import vd
6
+ import functools
7
+ import json
8
+
9
+ vd.option('fixer_api_key', '', 'API Key for api.apilayer.com/fixer')
10
+ vd.option('fixer_cache_days', 1, 'Cache days for currency conversions')
11
+
12
+ currency_symbols = {
13
+ '$': 'USD',
14
+ '£': 'GBP',
15
+ '₩': 'KRW',
16
+ '€': 'EUR',
17
+ '₪': 'ILS',
18
+ 'zł': 'PLN',
19
+ '₽': 'RUB',
20
+ '₫': 'VND',
21
+ }
22
+
23
+ def currency_rates_json(date='latest', base='USD'):
24
+ url = 'https://api.apilayer.com/fixer/%s?base=%s' % (date, base)
25
+ return vd.urlcache(
26
+ url,
27
+ days=vd.options.fixer_cache_days,
28
+ headers={
29
+ # First need to set some additional headers as otherwise apilayers will block it with a 403
30
+ # See also https://stackoverflow.com/questions/13303449/urllib2-httperror-http-error-403-forbidden
31
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11',
32
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
33
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
34
+ 'Accept-Encoding': 'none',
35
+ 'Accept-Language': 'en-US,en;q=0.8',
36
+ 'Connection': 'keep-alive',
37
+
38
+ # Finally set Apikey
39
+ 'apikey': vd.options.fixer_api_key
40
+ }
41
+ ).read_text()
42
+
43
+ @functools.lru_cache()
44
+ def currency_rates():
45
+ return json.loads(currency_rates_json())['rates']
46
+
47
+ @functools.lru_cache()
48
+ def currency_multiplier(src_currency, dest_currency):
49
+ 'returns equivalent value in USD for an amt of currency_code'
50
+ if src_currency == 'USD':
51
+ return 1.0
52
+ eur_usd_mult = currency_rates()['USD']
53
+ eur_src_mult = currency_rates()[src_currency]
54
+ usd_mult = eur_usd_mult/eur_src_mult
55
+ if dest_currency == 'USD':
56
+ return usd_mult
57
+
58
+ return usd_mult/currency_rates()[dest_currency]
59
+
60
+ def USD(s):
61
+ for currency_symbol, currency_code in currency_symbols.items():
62
+ if currency_symbol in s:
63
+ amt = float(s.replace(currency_symbol, ''))
64
+ return amt*currency_multiplier(currency_code, 'USD')
65
+
66
+ amtstr, currcode = s.split(' ')
67
+ return float(amtstr) * currency_multiplier(currcode, 'USD')
68
+
69
+
70
+ vd.addGlobals(USD=USD)
@@ -21,3 +21,5 @@ def customdate(sheet, fmtstr):
21
21
 
22
22
  Sheet.addCommand('z@', 'type-customdate', 'fmt=input("date format: ", type="fmtstr"); cursorCol.type=customdate(fmt); cursorCol.fmtstr=fmt', 'set type of current column to custom date format')
23
23
  ColumnsSheet.addCommand('gz@', 'type-customdate-selected', 'fmt=input("date format: ", type="fmtstr"); onlySelectedRows.type=customdate(fmt); onlySelectedRows.fmtstr=fmt', 'set type of selected columns to date')
24
+
25
+ vd.addMenuItems('Column > Type as > custom date format > type-customdate')
@@ -0,0 +1,132 @@
1
+ """
2
+ # Usage
3
+
4
+ Duplicates are determined by the sheet's key columns.
5
+
6
+ If no key columns are specified, then a duplicate row is one where the values
7
+ of *all non-hidden* columns are exactly the same as a row that occurs earlier
8
+ in the sheet.
9
+
10
+ If key columns *are* specified, then duplicates are detected based on the
11
+ values in just those columns.
12
+
13
+ ## Commands
14
+
15
+ - `select-duplicate-rows` sets the selection status in VisiData to `selected`
16
+ for each row in the active sheet that is a duplicate of a prior row.
17
+
18
+ - `dedupe-rows` pushes a new sheet in which only non-duplicate rows in the
19
+ active sheet are included.
20
+ """
21
+
22
+
23
+ __author__ = "Jeremy Singer-Vine <jsvine@gmail.com>"
24
+
25
+ from copy import copy
26
+
27
+ from visidata import Sheet, BaseSheet, asyncthread, Progress, vd
28
+
29
+
30
+ def gen_identify_duplicates(sheet):
31
+ """
32
+ Takes a sheet, and returns a generator yielding a tuple for each row
33
+ encountered. The tuple's structure is `(row_object, is_dupe)`, where
34
+ is_dupe is True/False.
35
+
36
+ See note in Usage section above regarding how duplicates are determined.
37
+ """
38
+
39
+ keyCols = sheet.keyCols
40
+
41
+ cols_to_check = None
42
+ if len(keyCols) == 0:
43
+ vd.warning("No key cols specified. Using all columns.")
44
+ cols_to_check = sheet.visibleCols
45
+ else:
46
+ cols_to_check = sheet.keyCols
47
+
48
+ seen = set()
49
+ for r in sheet.rows:
50
+ vals = tuple(col.getValue(r) for col in cols_to_check)
51
+ is_dupe = vals in seen
52
+ if not is_dupe:
53
+ seen.add(vals)
54
+ yield (r, is_dupe)
55
+
56
+
57
+ @Sheet.api
58
+ @asyncthread
59
+ def select_duplicate_rows(sheet, duplicates=True):
60
+ """
61
+ Given a sheet, sets the selection status in VisiData to `selected` for each
62
+ row that is a duplicate of a prior row.
63
+
64
+ If `duplicates = False`, then the behavior is reversed; sets the selection
65
+ status to `selected` for each row that is *not* a duplicate.
66
+ """
67
+ before = len(sheet.selectedRows)
68
+
69
+ gen = gen_identify_duplicates(sheet)
70
+ prog = Progress(gen, gerund="selecting", total=sheet.nRows)
71
+
72
+ for row, is_dupe in prog:
73
+ if is_dupe == duplicates:
74
+ sheet.selectRow(row)
75
+
76
+ sel_count = len(sheet.selectedRows) - before
77
+
78
+ more_str = " more" if before > 0 else ""
79
+
80
+ vd.status(f"selected {sel_count}{more_str} {sheet.rowtype}")
81
+
82
+
83
+ @Sheet.api
84
+ def dedupe_rows(sheet):
85
+ """
86
+ Given a sheet, pushes a new sheet in which only non-duplicate rows are
87
+ included.
88
+ """
89
+ vs = copy(sheet)
90
+ vs.name += "_deduped"
91
+
92
+ @asyncthread
93
+ def _reload(self=vs):
94
+ self.rows = []
95
+ gen = gen_identify_duplicates(sheet)
96
+ prog = Progress(gen, gerund="deduplicating", total=sheet.nRows)
97
+ for row, is_dupe in prog:
98
+ if not is_dupe:
99
+ self.addRow(row)
100
+
101
+ vs.reload = _reload
102
+ return vs
103
+
104
+
105
+ # Add longname-commands to VisiData to execute these methods
106
+ BaseSheet.addCommand(None, "select-duplicate-rows", "sheet.select_duplicate_rows()", "select each row that is a duplicate of a prior row")
107
+ BaseSheet.addCommand(None, "dedupe-rows", "vd.push(sheet.dedupe_rows())", "open new sheet in which only non-duplicate rows in the active sheet are included")
108
+
109
+ vd.addMenuItems('''
110
+ Row > Select > duplicate rows > select-duplicate-rows
111
+ Data > Deduplicate rows > dedupe-rows
112
+ ''')
113
+
114
+ """
115
+ # Changelog
116
+
117
+ ## 0.2.0 - 2021-09-22
118
+
119
+ Use `vd.warning(...)` instead of `warning(...)`
120
+
121
+ ## 0.1.0 - 2020-10-09
122
+
123
+ Revised for compatibility with VisiData 2.x
124
+
125
+ ## 0.0.1 - 2019-01-01
126
+
127
+ Internal change, no external effects: Migrates from ._selectedRows to .selectedRows.
128
+
129
+ ## 0.0.0 - 2018-12-30
130
+
131
+ Initial release.
132
+ """
@@ -2,12 +2,10 @@ from copy import copy
2
2
  from statistics import mode, median, mean, stdev
3
3
 
4
4
  from visidata import vd, Column, ColumnAttr, vlen, RowColorizer, asyncthread, Progress, wrapply
5
- from visidata import BaseSheet, TableSheet, ColumnsSheet
5
+ from visidata import BaseSheet, TableSheet, ColumnsSheet, SheetsSheet
6
6
 
7
- __all__ = ['DescribeSheet']
8
7
 
9
-
10
- vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet')
8
+ vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet', help=vd.help_aggregators)
11
9
 
12
10
 
13
11
  @Column.api
@@ -24,12 +22,19 @@ def isError(col, row):
24
22
 
25
23
  class DescribeColumn(Column):
26
24
  def __init__(self, name, **kwargs):
25
+ kwargs.setdefault('width', 10)
27
26
  super().__init__(name, getter=lambda col,srccol: col.sheet.describeData[srccol].get(col.expr, ''), expr=name, **kwargs)
28
27
 
29
28
 
30
29
  # rowdef: Column from source sheet
31
30
  class DescribeSheet(ColumnsSheet):
32
31
  # rowtype = 'columns'
32
+ guide = '''
33
+ # Describe Sheet
34
+ This `Describe Sheet` shows a few basic metrics over data in {sheet.displaySource}, with each column represented by a row.
35
+
36
+ For example, row {sheet.cursorRowIndex} describes the _{sheet.cursorRow.name}_ column, showing its minimum value, maximum value, mean, median, and other measures.
37
+ '''
33
38
  precious = True
34
39
  columns = [
35
40
  ColumnAttr('sheet', 'sheet', width=0),
@@ -49,17 +54,11 @@ class DescribeSheet(ColumnsSheet):
49
54
  ]
50
55
  nKeys = 2
51
56
 
52
- @asyncthread
53
- def reload(self):
54
- super().reload()
57
+ def loader(self):
58
+ super().loader()
55
59
  self.rows = [c for c in self.rows if not c.hidden]
56
60
  self.describeData = { col: {} for col in self.rows }
57
-
58
- self.columns = []
59
- for c in type(self).columns:
60
- self.addColumn(c)
61
-
62
- self.setKeys(self.columns[:self.nKeys])
61
+ self.resetCols()
63
62
 
64
63
  for aggrname in vd.options.describe_aggrs.split():
65
64
  self.addColumn(DescribeColumn(aggrname, type=float))
@@ -94,8 +93,8 @@ class DescribeSheet(ColumnsSheet):
94
93
  for func in [min, max, sum, median]: # use type
95
94
  d[func.__name__] = self.calcStatistic(d, func, vals)
96
95
  for aggrname in vd.options.describe_aggrs.split():
97
- func = vd.getGlobals()[aggrname]
98
- d[func.__name__] = self.calcStatistic(d, func, vals)
96
+ aggr = vd.aggregators[aggrname].funcValues
97
+ d[aggrname] = self.calcStatistic(d, aggr, vals)
99
98
 
100
99
  def calcStatistic(self, d, func, *args, **kwargs):
101
100
  r = wrapply(func, *args, **kwargs)
@@ -115,8 +114,11 @@ class DescribeSheet(ColumnsSheet):
115
114
 
116
115
  TableSheet.addCommand('I', 'describe-sheet', 'vd.push(DescribeSheet(sheet.name+"_describe", source=[sheet]))', 'open Describe Sheet with descriptive statistics for all visible columns')
117
116
  BaseSheet.addCommand('gI', 'describe-all', 'vd.push(DescribeSheet("describe_all", source=vd.stackedSheets))', 'open Describe Sheet with description statistics for all visible columns from all sheets')
117
+ SheetsSheet.addCommand('gI', 'describe-selected', 'vd.push(DescribeSheet("describe_all", source=selectedRows))', 'open Describe Sheet with all visible columns from selected sheets')
118
118
 
119
119
  DescribeSheet.addCommand('zs', 'select-cell', 'cursorRow.sheet.select(cursorValue)', 'select rows on source sheet which are being described in current cell')
120
120
  DescribeSheet.addCommand('zu', 'unselect-cell', 'cursorRow.sheet.unselect(cursorValue)', 'unselect rows on source sheet which are being described in current cell')
121
121
 
122
+ vd.addMenuItems('Data > Statistics > describe-sheet')
123
+
122
124
  vd.addGlobals({'DescribeSheet':DescribeSheet})
@@ -0,0 +1,26 @@
1
+ from visidata import GuideSheet, vd
2
+
3
+ class ErrorsGuide(GuideSheet):
4
+ guide_text='''# What was that error?
5
+
6
+ Status messages include [:warning]warnings[/] and [:error]errors[/].
7
+
8
+ A command may issue a [:warning]warning[/] status and continue running.
9
+ A command that [:warning]fails[/] or [:error]errors[/] is aborted.
10
+
11
+ ## Investigating errors further
12
+
13
+ If a Python Exception like [:error]RuntimeError[/] appears in the sidebar:
14
+
15
+ - {help.commands.error_recent}
16
+ - {help.commands.errors_all}
17
+
18
+ If [:note_type]{vd.options.note_format_exc}[/] or [:error]{vd.options.note_getter_exc}[/] appear inside a cell, it indicates an error happened during calculation, type-conversion, or formatting. When the cursor is on an error cell:
19
+
20
+ - {help.commands.error_cell}
21
+ '''
22
+
23
+
24
+
25
+ vd.addGuide('ErrorsSheet', ErrorsGuide)
26
+
@@ -0,0 +1,202 @@
1
+ import math
2
+ import random
3
+ import os.path
4
+ from functools import singledispatch
5
+
6
+ from visidata import vd, Sheet, asyncthread, Progress, Column, VisiData, deduceType, anytype, getitemdef, ColumnsSheet
7
+
8
+
9
+ @Sheet.api
10
+ def getSampleRows(sheet):
11
+ 'Return list of sample rows, including the cursor row as the first row.'
12
+
13
+ # do not include cursorRow in sample
14
+ ret = sheet.rows[:sheet.cursorRowIndex] + sheet.rows[sheet.cursorRowIndex+1:]
15
+
16
+ n = sheet.options.default_sample_size
17
+ if n != 0 and n < sheet.nRows:
18
+ vd.aside(f'sampling {n} rows')
19
+ ret = random.sample(ret, n)
20
+
21
+ return [sheet.cursorRow] + ret
22
+
23
+
24
+ @Sheet.api
25
+ def expandCols(sheet, cols, rows=None, depth=0):
26
+ 'expand all visible columns of containers to the given depth (0=fully)'
27
+ ret = []
28
+ if not rows:
29
+ rows = sheet.getSampleRows()
30
+
31
+ for col in cols:
32
+ newcols = col.expand(rows)
33
+ if depth != 1: # countdown not yet complete, or negative (indefinite)
34
+ ret.extend(sheet.expandCols(newcols, rows, depth-1))
35
+ return ret
36
+
37
+ @singledispatch
38
+ def _createExpandedColumns(sampleValue, col, rows):
39
+ '''By default, a column is not expandable. Supported container types for
40
+ sampleValue trigger alternate, type-specific expansions.'''
41
+ return []
42
+
43
+ @_createExpandedColumns.register(dict)
44
+ def _(sampleValue, col, vals):
45
+ '''Build a set of columns to add, using the first occurrence of each key to
46
+ determine column type'''
47
+ newcols = {}
48
+
49
+ for val in Progress(vals, 'expanding'):
50
+ if not isinstance(val, dict): # allow mixed-use columns
51
+ continue
52
+ colsToAdd = set(val).difference(newcols)
53
+ colsToAdd and newcols.update({
54
+ k: deduceType(v)
55
+ for k, v in val.items()
56
+ if k in colsToAdd
57
+ })
58
+
59
+ return [
60
+ ExpandedColumn(col.sheet.options.fmt_expand_dict % (col.name, k), type=v, origCol=col, expr=k)
61
+ for k, v in newcols.items()
62
+ ]
63
+
64
+ def _createExpandedColumnsNamedTuple(col, val):
65
+ return [
66
+ ExpandedColumn(col.sheet.options.fmt_expand_dict % (col.name, k), type=colType, origCol=col, expr=i)
67
+ for i, (k, colType) in enumerate(zip(val._fields, (deduceType(v) for v in val)))
68
+ ]
69
+
70
+ @_createExpandedColumns.register(list)
71
+ @_createExpandedColumns.register(tuple)
72
+ def _(sampleValue, col, vals):
73
+ '''Use the longest sequence to determine the number of columns we need to
74
+ create, and their presumed types. Ignore strings and exceptions. '''
75
+ def lenNoExceptions(v):
76
+ try:
77
+ if isinstance(v, str):
78
+ return 0
79
+ return len(v)
80
+ except Exception as e:
81
+ return 0
82
+
83
+ if hasattr(sampleValue, '_fields'): # looks like a namedtuple
84
+ return _createExpandedColumnsNamedTuple(col, vals[0])
85
+
86
+ longestSeq = max(vals, key=lenNoExceptions)
87
+ colTypes = [deduceType(v) for v in longestSeq]
88
+ return [
89
+ ExpandedColumn(col.sheet.options.fmt_expand_list % (col.name, k), type=colType, origCol=col, expr=k)
90
+ for k, colType in enumerate(colTypes)
91
+ ]
92
+
93
+
94
+ @Column.api
95
+ def expand(col, rows):
96
+ isNull = col.sheet.isNullFunc()
97
+ nonNulls = [
98
+ col.getTypedValue(row)
99
+ for row in rows
100
+ if not isNull(col.getValue(row))
101
+ ]
102
+
103
+ if not nonNulls:
104
+ return []
105
+
106
+ # The type of the first non-null value for col determines if and how the
107
+ # column can be expanded.
108
+ expandedCols = _createExpandedColumns(nonNulls[0], col, nonNulls)
109
+
110
+ idx = col.sheet.columns.index(col)
111
+
112
+ for i, c in enumerate(expandedCols):
113
+ col.sheet.addColumn(c, index=idx+i+1)
114
+ if expandedCols:
115
+ col.hide()
116
+ return expandedCols
117
+
118
+
119
+ @VisiData.api
120
+ class ExpandedColumn(Column):
121
+ def calcValue(self, row):
122
+ return getitemdef(self.origCol.getValue(row), self.expr)
123
+
124
+ def setValue(self, row, value):
125
+ self.origCol.getValue(row)[self.expr] = value
126
+
127
+
128
+ @Sheet.api
129
+ @asyncthread
130
+ def contract_cols(sheet, cols, depth=1): # depth == 0 means contract all the way
131
+ 'Remove any columns in cols with .origCol, and also remove others in sheet.columns which share those .origCol. The inverse of expand.'
132
+ vd.addUndo(setattr, sheet, 'columns', sheet.columns)
133
+ for i in range(depth or 10000):
134
+ colsToClose = [c for c in cols if getattr(c, "origCol", None)]
135
+
136
+ if not colsToClose:
137
+ break
138
+
139
+ origCols = set(c.origCol for c in colsToClose)
140
+ for col in origCols:
141
+ col.width = sheet.options.default_width
142
+
143
+ sheet.columns = [col for col in sheet.columns if getattr(col, 'origCol', None) not in origCols]
144
+
145
+
146
+ @Sheet.api
147
+ @asyncthread
148
+ def expand_cols_deep(sheet, cols, rows=None, depth=0): # depth == 0 means drill all the way
149
+ return sheet.expandCols(cols, rows=rows, depth=depth)
150
+
151
+
152
+ @ColumnsSheet.api
153
+ def contract_source_cols(sheet, cols):
154
+ prefix = os.path.commonprefix([c.name for c in cols])
155
+ ret = ColumnGroup(prefix or 'group', prefix=prefix, sourceCols=cols)
156
+ for c in cols:
157
+ c.origCol = ret
158
+ for vs in sheet.source:
159
+ vd.addUndo(setattr, vs, 'columns', vs.columns)
160
+ vs.columns[:] = [c for c in vs.columns if c not in cols]
161
+ return ret
162
+
163
+
164
+ class ColumnGroup(Column):
165
+ def calcValue(self, row):
166
+ return {c.name[len(self.prefix):]:c.getValue(row) for c in self.sourceCols}
167
+
168
+ def expand(self, rows):
169
+ idx = self.sheet.columns.index(self)
170
+
171
+ for i, c in enumerate(self.sourceCols):
172
+ self.sheet.addColumn(c, index=idx+i+1)
173
+
174
+ self.hide()
175
+
176
+ return self.sourceCols
177
+
178
+
179
+ Sheet.addCommand('(', 'expand-col', 'expand_cols_deep([cursorCol], depth=1)', 'expand current column of containers one level')
180
+ Sheet.addCommand('g(', 'expand-cols', 'expand_cols_deep(visibleCols, depth=1)', 'expand all visible columns of containers one level')
181
+ Sheet.addCommand('z(', 'expand-col-depth', 'expand_cols_deep([cursorCol], depth=int(input("expand depth=", value=0)))', 'expand current column of containers to given depth (0=fully)')
182
+ Sheet.addCommand('gz(', 'expand-cols-depth', 'expand_cols_deep(visibleCols, depth=int(input("expand depth=", value=0)))', 'expand all visible columns of containers to given depth (0=fully)')
183
+
184
+ Sheet.addCommand(')', 'contract-col', 'contract_cols([cursorCol])', 'remove current column and siblings from sheet columns and unhide parent')
185
+ Sheet.addCommand('g)', 'contract-cols', 'contract_cols(visibleCols)', 'remove all child columns and unhide toplevel parents')
186
+ Sheet.addCommand('z)', 'contract-col-depth', 'contract_cols([cursorCol], depth=int(input("contract depth=", value=0)))', 'remove current column and siblings from sheet columns and unhide parent')
187
+ Sheet.addCommand('gz)', 'contract-cols-depth', 'contract_cols(visibleCols, depth=int(input("contract depth=", value=0)))', 'remove all child columns and unhide toplevel parents')
188
+
189
+ ColumnsSheet.addCommand(')', 'contract-source-cols', 'source[0].addColumn(contract_source_cols(someSelectedRows), index=cursorRowIndex)', 'contract selected columns into column group') #1702
190
+
191
+
192
+ vd.addMenuItems('''
193
+ Column > Expand > one level > expand-col
194
+ Column > Expand > to depth N > expand-col-depth
195
+ Column > Expand > all columns one level > expand-cols
196
+ Column > Expand > all columns to depth > expand-cols-depth
197
+ Column > Contract > one level > contract-col
198
+ Column > Contract > N levels > contract-col-depth
199
+ Column > Contract > all columns one level > contract-cols
200
+ Column > Contract > all columns N levels > contract-cols-depth
201
+ Column > Contract > selected columns on source sheet > contract-source-cols
202
+ ''')
@@ -1,4 +1,4 @@
1
- from visidata import *
1
+ from visidata import vd, VisiData, asyncthread, Sheet, Progress
2
2
 
3
3
 
4
4
  @VisiData.api
@@ -34,3 +34,5 @@ def fillNullValues(vd, col, rows):
34
34
 
35
35
 
36
36
  Sheet.addCommand('f', 'setcol-fill', 'fillNullValues(cursorCol, someSelectedRows)', 'fills null cells in selected rows of current column with contents of non-null cells up the current column')
37
+
38
+ vd.addMenuItems('Column > Fill > setcol-fill')
@@ -1,5 +1,6 @@
1
- from visidata import *
2
1
  import collections
2
+ from visidata import Column, Sheet, VisiData, ColumnItem, Progress, TypedExceptionWrapper, SettableColumn
3
+ from visidata import asyncthread, vd
3
4
 
4
5
 
5
6
  @Column.api
@@ -35,6 +36,7 @@ class StaticSheet(Sheet):
35
36
  def __init__(self, source):
36
37
  super().__init__(source.name + "'", source=source)
37
38
 
39
+ def resetCols(self):
38
40
  self.columns = []
39
41
  for i, col in enumerate(self.source.visibleCols):
40
42
  colcopy = ColumnItem(col.name)
@@ -44,12 +46,12 @@ class StaticSheet(Sheet):
44
46
  if col in self.source.keyCols:
45
47
  self.setKeys([colcopy])
46
48
 
47
- @asyncthread
48
- def reload(self):
49
- self.rows = []
49
+ def iterload(self):
50
50
  for r in Progress(self.source.rows, 'calculating'):
51
51
  row = []
52
- self.addRow(row)
52
+ yield row
53
+
54
+ # now fill out row
53
55
  for col in self.source.visibleCols:
54
56
  val = col.getTypedValue(r)
55
57
  if isinstance(val, TypedExceptionWrapper):
@@ -58,7 +60,10 @@ class StaticSheet(Sheet):
58
60
  row.append(val)
59
61
 
60
62
 
61
- Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(StaticColumn(cursorCol))', 'add a frozen copy of current column with all cells evaluated')
63
+ Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(freeze_col(cursorCol))', 'add a frozen copy of current column with all cells evaluated')
62
64
  Sheet.addCommand("g'", 'freeze-sheet', 'vd.push(StaticSheet(sheet)); status("pushed frozen copy of "+name)', 'open a frozen copy of current sheet with all visible columns evaluated')
63
65
  Sheet.addCommand("z'", 'cache-col', 'cursorCol.resetCache()', 'add/reset cache for current column')
64
66
  Sheet.addCommand("gz'", 'cache-cols', 'for c in visibleCols: c.resetCache()', 'add/reset cache for all visible columns')
67
+
68
+ vd.addMenuItem('Column', 'Freeze', 'freeze-col')
69
+ vd.addMenuItem('File', 'Freeze', 'freeze-sheet')