visidata 2.11.1__py3-none-any.whl → 3.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 (255) 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 +65 -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 +59 -50
  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 +5 -1
  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 +30 -5
  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 +163 -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} +75 -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 +180 -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 +17 -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 +48 -10
  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 +301 -148
  187. visidata/man/vd.txt +290 -153
  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 +50 -201
  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 +229 -257
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +113 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/conftest.py +3 -3
  213. visidata/tests/test_cliptext.py +39 -0
  214. visidata/tests/test_commands.py +62 -7
  215. visidata/tests/test_edittext.py +2 -2
  216. visidata/tests/test_features.py +17 -0
  217. visidata/tests/test_menu.py +14 -0
  218. visidata/tests/test_path.py +13 -4
  219. visidata/text_source.py +53 -0
  220. visidata/textsheet.py +10 -3
  221. visidata/theme.py +44 -0
  222. visidata/themes/__init__.py +0 -0
  223. visidata/themes/ascii8.py +84 -0
  224. visidata/themes/asciimono.py +84 -0
  225. visidata/themes/light.py +17 -0
  226. visidata/threads.py +87 -39
  227. visidata/tuiwin.py +22 -0
  228. visidata/type_currency.py +22 -3
  229. visidata/type_date.py +31 -9
  230. visidata/type_floatsi.py +5 -1
  231. visidata/undo.py +17 -5
  232. visidata/utils.py +106 -23
  233. visidata/vdobj.py +28 -17
  234. visidata/windows.py +10 -0
  235. visidata/wrappers.py +9 -3
  236. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  237. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
  238. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
  239. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  240. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
  241. visidata-3.0.dist-info/RECORD +257 -0
  242. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  243. vgit/__init__.py +0 -1
  244. vgit/gitsheet.py +0 -164
  245. visidata/layout.py +0 -44
  246. visidata/misc.py +0 -5
  247. visidata-2.11.1.data/scripts/vgit +0 -9
  248. visidata-2.11.1.dist-info/RECORD +0 -155
  249. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  250. {vgit → visidata/apps/vgit}/abort.py +0 -0
  251. /visidata/{repeat.py → features/repeat.py} +0 -0
  252. {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
  253. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,306 @@
1
+ '''# RedditSheet
2
+
3
+ - [:keys]Ctrl+O[/] to open a browser tab to [:code]{sheet.cursorRow.display_name_prefixed}[/]
4
+ - [:keys]g Ctrl+O[/] to open browser windows for {sheet.nSelectedRows} selected subreddits
5
+
6
+ - [:keys]Enter[/] to open sheet with top ~1000 submissions for [:code]{sheet.cursorRow.display_name_prefixed}[/]
7
+ - [:keys]g Enter[/] to open sheet with top ~1000 submissions for {sheet.nSelectedRows} selected subreddits
8
+
9
+ - [:keys]ga[/] to append more subreddits matching input by name or description
10
+ '''
11
+
12
+ import visidata
13
+ from visidata import vd, VisiData, Sheet, AttrColumn, asyncthread, ENTER, anytype, date
14
+
15
+
16
+ vd.option('reddit_client_id', '', 'client_id for reddit api')
17
+ vd.option('reddit_client_secret', '', 'client_secret for reddit api')
18
+ vd.option('reddit_user_agent', visidata.__version__, 'user_agent for reddit api')
19
+
20
+
21
+ @VisiData.api
22
+ def open_reddit(vd, p):
23
+ vd.importExternal('praw')
24
+ vd.enable_requests_cache()
25
+
26
+ if not vd.options.reddit_client_id:
27
+ return RedditGuide('reddit_guide')
28
+
29
+ if p.given.startswith('r/') or p.given.startswith('/r/'):
30
+ return SubredditSheet(p.base_stem, source=p.base_stem.split('+'), search=(p.given[0]=='/'))
31
+
32
+ if p.given.startswith('u/') or p.given.startswith('/u/'):
33
+ return RedditorsSheet(p.base_stem, source=p.base_stem.split('+'), search=(p.given[0]=='/'))
34
+
35
+ return SubredditSheet(p.base_stem, source=p)
36
+
37
+ vd.new_reddit = vd.open_reddit
38
+
39
+ @VisiData.cached_property
40
+ def reddit(vd):
41
+ import praw
42
+ return praw.Reddit(check_for_updates=False, **vd.options.getall('reddit_'))
43
+
44
+
45
+ subreddit_hidden_attrs='''
46
+ name #accounts_active accounts_active_is_fuzzed advertiser_category
47
+ all_original_content allow_chat_post_creation allow_discovery
48
+ allow_galleries allow_images allow_polls allow_predictions
49
+ allow_predictions_tournament allow_videogifs allow_videos
50
+ banner_background_color banner_background_image banner_img banner_size
51
+ can_assign_link_flair can_assign_user_flair collapse_deleted_comments
52
+ comment_score_hide_mins community_icon community_reviewed @created
53
+ @created_utc description_html disable_contributor_requests
54
+ display_name display_name_prefixed emoji emojis_custom_size
55
+ emojis_enabled filters free_form_reports fullname has_menu_widget
56
+ header_img header_size header_title hide_ads icon_img icon_size
57
+ is_chat_post_feature_enabled is_crosspostable_subreddit
58
+ is_enrolled_in_new_modmail key_color lang link_flair_enabled
59
+ link_flair_position mobile_banner_image mod notification_level
60
+ original_content_tag_enabled over18 prediction_leaderboard_entry_type
61
+ primary_color public_description public_description_html public_traffic
62
+ quaran quarantine restrict_commenting restrict_posting show_media
63
+ show_media_preview spoilers_enabled submission_type submit_link_label
64
+ submit_text submit_text_html submit_text_label suggested_comment_sort
65
+ user_can_flair_in_sr user_flair_background_color user_flair_css_class
66
+ user_flair_enabled_in_sr user_flair_position user_flair_richtext
67
+ user_flair_template_id user_flair_text user_flair_text_color
68
+ user_flair_type user_has_favorited user_is_banned user_is_contributor
69
+ user_is_moderator user_is_muted user_is_subscriber user_sr_flair_enabled
70
+ user_sr_theme_enabled #videostream_links_count whitelist_status widgets
71
+ wiki wiki_enabled wls
72
+ '''
73
+
74
+ post_hidden_attrs='''
75
+ all_awardings allow_live_comments @approved_at_utc approved_by archived
76
+ author_flair_background_color author_flair_css_class author_flair_richtext
77
+ author_flair_template_id author_flair_text author_flair_text_color
78
+ author_flair_type author_fullname author_patreon_flair author_premium
79
+ awarders @banned_at_utc banned_by can_gild can_mod_post category clicked
80
+ comment_limit comment_sort content_categories contest_mode @created_utc
81
+ discussion_type distinguished domain edited flair fullname gilded
82
+ gildings hidden hide_score is_crosspostable is_meta is_original_content
83
+ is_reddit_media_domain is_robot_indexable is_self is_video likes
84
+ link_flair_background_color link_flair_css_class link_flair_richtext
85
+ link_flair_text link_flair_text_color link_flair_type locked media
86
+ media_embed media_only mod mod_note mod_reason_by mod_reason_title
87
+ mod_reports name no_follow num_crossposts num_duplicates num_reports
88
+ over_18 parent_whitelist_status permalink pinned pwls quarantine
89
+ removal_reason removed_by removed_by_category report_reasons saved
90
+ score secure_media secure_media_embed selftext_html send_replies
91
+ shortlink spoiler stickied subreddit_id subreddit_name_prefixed
92
+ subreddit_subscribers subreddit_type suggested_sort thumbnail
93
+ thumbnail_height thumbnail_width top_awarded_type total_awards_received
94
+ treatment_tags upvote_ratio user_reports #view_count visited
95
+ whitelist_status wls
96
+ '''
97
+
98
+ comment_hidden_attrs='''
99
+ all_awardings @approved_at_utc approved_by archived associated_award
100
+ author_flair_background_color author_flair_css_class author_flair_richtext
101
+ author_flair_template_id author_flair_text author_flair_text_color
102
+ author_flair_type author_fullname author_patreon_flair author_premium
103
+ awarders @banned_at_utc banned_by body_html can_gild can_mod_post
104
+ collapsed collapsed_because_crowd_control collapsed_reason comment_type
105
+ controversiality @created_utc distinguished fullname gilded gildings
106
+ is_root is_submitter likes link_id locked mod mod_note mod_reason_by
107
+ mod_reason_title mod_reports name no_follow num_reports parent_id
108
+ permalink removal_reason report_reasons saved #score #score_hidden
109
+ send_replies stickied submission subreddit_id subreddit_name_prefixed
110
+ subreddit_type top_awarded_type total_awards_received treatment_tags
111
+ user_reports
112
+ '''
113
+
114
+ redditor_hidden_attrs='''
115
+ #awardee_karma #awarder_karma @created @created_utc
116
+ fullname has_subscribed has_verified_email hide_from_robots icon_img id
117
+ is_employee is_friend is_gold is_mod pref_show_snoovatar
118
+ snoovatar_img snoovatar_size stream #total_karma verified
119
+ subreddit.banner_img subreddit.name subreddit.over_18 subreddit.public_description #subreddit.subscribers subreddit.title
120
+ '''
121
+
122
+ def hiddenCols(hidden_attrs):
123
+ coltypes = { t.icon:t.typetype for t in vd.typemap.values() if not t.icon.isalpha() }
124
+ for attr in hidden_attrs.split():
125
+ coltype = anytype
126
+ if attr[0] in coltypes:
127
+ coltype = coltypes.get(attr[0])
128
+ attr = attr[1:]
129
+ yield AttrColumn(attr, type=coltype, width=0)
130
+
131
+
132
+ class SubredditSheet(Sheet):
133
+ guide = __doc__
134
+ # source is a text list of subreddits
135
+ rowtype = 'subreddits' # rowdef: praw.Subreddit
136
+ nKeys=1
137
+ search=False
138
+ columns = [
139
+ AttrColumn('display_name_prefixed', width=15),
140
+ AttrColumn('active_user_count', type=int),
141
+ AttrColumn('subscribers', type=int),
142
+ AttrColumn('subreddit_type'),
143
+ AttrColumn('title'),
144
+ AttrColumn('description', width=50),
145
+ AttrColumn('url', width=10),
146
+ ] + list(hiddenCols(subreddit_hidden_attrs))
147
+
148
+ def iterload(self):
149
+ for name in self.source:
150
+ name = name.strip()
151
+ if self.search:
152
+ yield from vd.reddit.subreddits.search(name)
153
+ else:
154
+ try:
155
+ r = vd.reddit.subreddit(name)
156
+ r.display_name_prefixed
157
+ yield r
158
+ except Exception as e:
159
+ vd.exceptionCaught(e)
160
+
161
+ def openRow(self, row):
162
+ return RedditSubmissions(row.display_name_prefixed, source=row)
163
+
164
+ def openRows(self, rows):
165
+ comboname = '+'.join(row.display_name for row in rows)
166
+ return RedditSubmissions(comboname, source=vd.reddit.subreddit(comboname))
167
+
168
+
169
+ class RedditorsSheet(Sheet):
170
+ # source is a text list of usernames
171
+ rowtype = 'redditors' # rowdef: praw.Subreddit
172
+ nKeys=1
173
+ columns = [
174
+ AttrColumn('name', width=15),
175
+ AttrColumn('comment_karma', type=int),
176
+ AttrColumn('link_karma', type=int),
177
+ AttrColumn('comments'),
178
+ AttrColumn('submissions'),
179
+ ] + list(hiddenCols(redditor_hidden_attrs))
180
+
181
+ def iterload(self):
182
+ for name in self.source:
183
+ if self.search:
184
+ yield from vd.reddit.redditors.popular(name)
185
+ else:
186
+ yield vd.reddit.redditor(name)
187
+
188
+ def openRow(self, row):
189
+ return RedditSubmissions(row.fullname, source=row.submissions)
190
+
191
+ def openRows(self, rows):
192
+ comboname = '+'.join(row.name for row in rows)
193
+ return RedditSubmissions(comboname, source=vd.reddit.redditor(comboname).submissions)
194
+
195
+
196
+ class RedditSubmissions(Sheet):
197
+ guide = '''# Reddit Submissions
198
+
199
+ [:keys]Enter[/] to open sheet with comments for the current post
200
+ [:keys]ga[/] to add posts in this subreddit matching input'''
201
+
202
+ # source=ListingGenerator
203
+ rowtype='reddit posts' # rowdef: praw.Submission
204
+ nKeys=2
205
+ columns = [
206
+ AttrColumn('subreddit'),
207
+ AttrColumn('id', width=0),
208
+ AttrColumn('created', width=12, type=date),
209
+ AttrColumn('author'),
210
+ AttrColumn('ups', width=8, type=int),
211
+ AttrColumn('downs', width=8, type=int),
212
+ AttrColumn('num_comments', width=8, type=int),
213
+ AttrColumn('title', width=50),
214
+ AttrColumn('selftext', width=60),
215
+ AttrColumn('url'),
216
+ AttrColumn('comments', width=0),
217
+ ] + list(hiddenCols(post_hidden_attrs))
218
+
219
+ def iterload(self):
220
+ kind = 'new' # 'top'
221
+ f = getattr(self.source, kind, None)
222
+ if f:
223
+ yield from f(limit=10000)
224
+
225
+ def openRow(self, row):
226
+ return RedditComments(row.id, source=row.comments.list())
227
+
228
+
229
+ class RedditComments(Sheet):
230
+ # source=list of comments
231
+ rowtype='comments' # rowdef: praw.Comment
232
+ nKeys=2
233
+ columns=[
234
+ AttrColumn('subreddit', width=0),
235
+ AttrColumn('id', width=0),
236
+ AttrColumn('ups', width=4, type=int),
237
+ AttrColumn('downs', width=4, type=int),
238
+ AttrColumn('replies', type=list),
239
+ AttrColumn('created', type=date),
240
+ AttrColumn('author'),
241
+ AttrColumn('depth', type=int),
242
+ AttrColumn('body', width=60),
243
+ AttrColumn('edited', width=0),
244
+ ] + list(hiddenCols(comment_hidden_attrs))
245
+
246
+ def iterload(self):
247
+ yield from self.source
248
+
249
+ def openRow(self, row):
250
+ return RedditComments(row.id, source=row.replies)
251
+
252
+
253
+ class RedditGuide(RedditSubmissions):
254
+ guide = '''# Authenticate Reddit
255
+ The Reddit API must be configured before use.
256
+
257
+ 1. Login to Reddit and go to [:underline]https://www.reddit.com/prefs/apps[/].
258
+ 2. Create a "script" app. (Use "[:underline]http://localhost:8000[/]" for the redirect uri)
259
+ 3. Add credentials to visidatarc:
260
+
261
+ options.reddit_client_id = '...' # below the description in the upper left
262
+ options.reddit_client_secret = '...'
263
+
264
+ ## Use [:code]reddit[/] filetype for subreddits or users
265
+
266
+ Multiple may be specified, joined with "+".
267
+
268
+ vd r/commandline.reddit
269
+ vd u/gallowboob.reddit
270
+ vd r/rust+golang+python.reddit
271
+ vd u/spez+kn0thing.reddit
272
+ '''
273
+
274
+ @SubredditSheet.api
275
+ @asyncthread
276
+ def addRowsFromQuery(sheet, q):
277
+ for r in vd.reddit.subreddits.search(q):
278
+ sheet.addRow(r, index=sheet.cursorRowIndex+1)
279
+
280
+
281
+ @RedditSubmissions.api
282
+ @asyncthread
283
+ def addRowsFromQuery(sheet, q):
284
+ for r in sheet.source.search(q, limit=None):
285
+ sheet.addRow(r, index=sheet.cursorRowIndex+1)
286
+
287
+
288
+ @VisiData.api
289
+ def sysopen_subreddits(vd, *subreddits):
290
+ url = "https://www.reddit.com/r/"+"+".join(subreddits)
291
+ vd.launchBrowser(url)
292
+
293
+
294
+ SubredditSheet.addCommand('^O', 'sysopen-subreddit', 'sysopen_subreddits(cursorRow.display_name)', 'open browser window with subreddit')
295
+ SubredditSheet.addCommand('g^O', 'sysopen-subreddits', 'sysopen_subreddits(*(row.display_name for row in selectedRows))', 'open browser window with messages from selected subreddits')
296
+ SubredditSheet.addCommand('g'+ENTER, 'open-subreddits', 'vd.push(openRows(selectedRows))', 'open sheet with top ~1000 submissions for each selected subreddit')
297
+ SubredditSheet.addCommand('ga', 'add-subreddits-match', 'addRowsFromQuery(input("add subreddits matching: "))', 'add subreddits matching input by name or description')
298
+ RedditSubmissions.addCommand('ga', 'add-submissions-match', 'addRowsFromQuery(input("add posts matching: "))', 'add posts in this subreddit matching input')
299
+
300
+ vd.addMenuItems('''
301
+ File > Reddit > open selected subreddits > open-subreddits
302
+ File > Reddit > add > matching subreddits > add-subreddits-match
303
+ File > Reddit > add > matching submissions > add-submissions-match
304
+ File > Reddit > open in browser > subreddit in current row > sysopen-subreddit
305
+ File > Reddit > open in browser > selected subreddits > sysopen-subreddits
306
+ ''')
@@ -0,0 +1,249 @@
1
+ import time
2
+
3
+ from visidata import vd, VisiData, BaseSheet, Sheet, TextSheet, PyobjSheet
4
+ from visidata import ItemColumn, Column, vlen, date, asyncsingle, ENTER, AttrDict
5
+
6
+ vd.option('zulip_batch_size', -100, 'number of messages to fetch per call (<0 to fetch before anchor)')
7
+ vd.option('zulip_anchor', 1000000000, 'message id to start fetching from')
8
+ vd.option('zulip_delay_s', 0.00001, 'seconds to wait between calls (0 to stop after first)')
9
+ vd.option('zulip_api_key', '', 'Zulip API key')
10
+ vd.option('zulip_email', '', 'Email for use with Zulip API key')
11
+
12
+
13
+ @VisiData.api
14
+ def open_zulip(vd, p):
15
+ vd.importExternal('zulip')
16
+ import zulip
17
+
18
+ if not vd.options.zulip_api_key:
19
+ vd.warning('zulip_api_key must be set first')
20
+ vd.status('Enter your login email and Zulip API key (see _https://zulip.com/api/api-keys_).')
21
+ email = vd.input(f'Login email for {p.given}: ', record=False)
22
+ api_key = vd.input(f'Zulip API key: ', record=False)
23
+
24
+ vd.setPersistentOptions(zulip_email=email, zulip_api_key=api_key)
25
+
26
+ vd.z_client = zulip.Client(site=p.given, api_key=vd.options.zulip_api_key, email=vd.options.zulip_email)
27
+
28
+ return vd.subscribedStreams
29
+
30
+
31
+ VisiData.openhttp_zulip = VisiData.open_zulip
32
+
33
+
34
+ @VisiData.api
35
+ def z_rpc(vd, r, result_field_name=None):
36
+ if r['result'] != 'success':
37
+ return PyobjSheet(result_field_name+'_error', source=r)
38
+ elif result_field_name:
39
+ return PyobjSheet(result_field_name, source=r[result_field_name])
40
+
41
+
42
+ @VisiData.lazy_property
43
+ def allStreams(vd):
44
+ return ZulipStreamsSheet("all_streams", zulip_func='get_streams', zulip_result_key="streams", zulip_kwargs=dict(include_public=True, include_subscribed=True))
45
+
46
+
47
+ @VisiData.lazy_property
48
+ def subscribedStreams(vd):
49
+ return ZulipStreamsSheet("subscriptions", zulip_func='get_subscriptions', zulip_result_key="subscriptions")
50
+
51
+
52
+ @VisiData.lazy_property
53
+ def allMessages(vd):
54
+ return ZulipMessagesSheet("all_messages")
55
+
56
+
57
+ @VisiData.api
58
+ def parseColumns(vd, fieldlist):
59
+ for cname in fieldlist:
60
+ kwargs = {}
61
+ while not cname[0].isalpha():
62
+ if cname[0] == '#': kwargs['type'] = int
63
+ elif cname[0] == '@': kwargs['type'] = date
64
+ elif cname[0] == '-': kwargs['width'] = 0
65
+ else: break
66
+ cname = cname[1:]
67
+ yield ItemColumn(cname, **kwargs)
68
+
69
+
70
+ class ZulipAPISheet(Sheet):
71
+ zulip_func = None
72
+ zulip_result_key = ''
73
+ zulip_args = []
74
+ zulip_kwargs = {}
75
+ fields = ''
76
+
77
+ def iterload(self):
78
+ self.columns = []
79
+ for c in vd.parseColumns(self.fields.split()):
80
+ self.addColumn(c)
81
+
82
+ zulip_func = self.zulip_func
83
+ if isinstance(zulip_func, str): # allow later binding for startup perf
84
+ zulip_func = getattr(vd.z_client, zulip_func)
85
+ r = zulip_func(*self.zulip_args, **self.zulip_kwargs)
86
+ if r['result'] != 'success':
87
+ vd.push(PyobjSheet(self.zulip_result_key+'_error', source=r))
88
+ return
89
+ yield from r[self.zulip_result_key]
90
+
91
+ def addRow(self, r, **kwargs):
92
+ return super().addRow(AttrDict(r), **kwargs)
93
+
94
+
95
+ class ZulipStreamsSheet(ZulipAPISheet):
96
+ guide = '''# Zulip Streams
97
+
98
+ - `Enter` to open recent messages from the stream
99
+ - `z Enter` to open list of topics from the stream
100
+ '''
101
+ rowtype = 'streams' # rowdef: dict of stream from server
102
+ fields = '-#stream_id name @date_created description -rendered_description -invite_only -is_web_public -stream_post_policy -history_public_to_subscribers -#first_message_id -#message_retention_days -is_announcement_only'
103
+
104
+ def openRow(self, r):
105
+ return ZulipMessagesSheet(r.name, filters=dict(stream=r.name))
106
+
107
+ def openCell(self, c, r):
108
+ return ZulipTopicsSheet(r.name+'_topics',
109
+ zulip_func=vd.z_client.get_stream_topics,
110
+ zulip_args=[r.stream_id],
111
+ zulip_result_key='topics')
112
+
113
+
114
+ class ZulipTopicsSheet(ZulipAPISheet):
115
+ rowtype = 'topics' # rowdef: dict of topic from server
116
+ fields='name #max_id'
117
+ def openRow(self, r):
118
+ return ZulipMessagesSheet(f'{r.name}:{r.subject}', filters=dict(stream=r.name, topic=r.subject))
119
+
120
+
121
+ class ZulipMembersSheet(ZulipAPISheet):
122
+ guide = '''# Zulip Members
123
+ - `Enter` to open list of messages from this member
124
+ '''
125
+ rowtype = 'members' # rowdef: dict of member from server
126
+ fields = '''-#user_id full_name email timezone @date_joined -#avatar_version -is_admin -is_owner -is_guest -is_bot -#role -is_active -avatar_url -bot_type -#bot_owner_id'''
127
+ def openRow(self, r):
128
+ return ZulipMessagesSheet(r.display_recipient, filters=dict(stream=r.display_recipient))
129
+
130
+
131
+ class ZulipMessagesSheet(Sheet):
132
+ guide = '''# Zulip Messages Sheet
133
+ Loads continuously starting with most recent, until all messages have been read.
134
+
135
+ - `Ctrl+C` to cancel loading.
136
+ - `Enter` to open message in word-wrapped text sheet
137
+ '''
138
+ rowtype = 'messages' # rowdef: dict of message from server
139
+ # fields = ''
140
+ columns = [
141
+ ItemColumn('timestamp', type=date, fmtstr='%Y-%m-%d %H:%M'),
142
+ ItemColumn('sender', 'sender_full_name'),
143
+ ItemColumn('sender_email', width=0),
144
+ ItemColumn('recipient', 'display_recipient'),
145
+ ItemColumn('subject'),
146
+ ItemColumn('content'),
147
+ ItemColumn('client', width=0),
148
+ ItemColumn('reactions', type=vlen),
149
+ ItemColumn('submessages', type=vlen),
150
+ ItemColumn('flags', width=0),
151
+ ]
152
+ filters={}
153
+
154
+ @asyncsingle # kill previous thread
155
+ def reload(self):
156
+ self.rows = []
157
+ narrow = list(self.filters.items())
158
+ n = self.options.zulip_batch_size
159
+ req = AttrDict(
160
+ num_before = -n if n < 0 else 0,
161
+ num_after = n if n > 0 else 0,
162
+ anchor = self.options.zulip_anchor,
163
+ apply_markdown = False,
164
+ narrow = narrow)
165
+
166
+ while True:
167
+ r = vd.z_client.call_endpoint(url='messages', method='GET', request=req)
168
+ if r['result'] == 'success':
169
+ if not r['messages']: break
170
+ for i, msg in enumerate(r['messages']):
171
+ self.addRow(msg, index=i)
172
+ req['anchor'] = min(msg['id'] for msg in r['messages'])-1
173
+ s = self.options.zulip_delay_s
174
+ if s <= 0:
175
+ break
176
+ time.sleep(s)
177
+
178
+ # vd.status('finished loading, waiting for new messages')
179
+ # vd.z_client.call_on_each_event(self.received_event, ['message'], narrow=narrow)
180
+
181
+ def get_channel_name(self, r):
182
+ recp = r['display_recipient']
183
+ if isinstance(recp, list): # private message
184
+ return '[%s]' % recp[0]['full_name']
185
+ else:
186
+ return '%s:%s' % (recp, r['subject'])
187
+
188
+ def update_message(self, msgid, content):
189
+ req = {
190
+ "message_id": msgid,
191
+ "content": content
192
+ }
193
+ vd.z_rpc(vd.z_client.update_message(req))
194
+
195
+ def openRow(self, r):
196
+ vs = TextSheet(self.get_channel_name(r), source=[r["content"]])
197
+ vs.options.wrap = True
198
+ return vs
199
+
200
+ def received_event(self, event):
201
+ if event['type'] == 'message':
202
+ self.addRow(event['message'])
203
+
204
+ def reply_message(self, msg, row):
205
+ recp = row['display_recipient']
206
+ if isinstance(recp, list):
207
+ for dest in recp:
208
+ self.send_message(msg, row['subject'], dest['email'], 'private')
209
+ else:
210
+ self.send_message(msg, row['subject'], dest, 'stream')
211
+
212
+ def send_message(self, msg, subject, dest, msgtype='stream'):
213
+ req = {
214
+ 'type': msgtype,
215
+ 'content': msg,
216
+ 'subject': subject,
217
+ 'to': dest,
218
+ }
219
+ r = vd.z_client.send_message(req)
220
+
221
+ if r['result'] != 'success':
222
+ vd.push(PyobjSheet('send_message_result', source=r))
223
+
224
+
225
+ vd.addGlobals({
226
+ 'ZulipMembersSheet': ZulipMembersSheet,
227
+ 'ZulipStreamsSheet': ZulipStreamsSheet,
228
+ 'ZulipAPISheet': ZulipAPISheet,
229
+ 'ZulipMessagesSheet': ZulipMessagesSheet,
230
+ })
231
+
232
+ ZulipAPISheet.addCommand('', 'open-zulip-profile', 'vd.push(PyobjSheet("profile", source=z_client.get_profile()))', 'open connected user\'s profile')
233
+ ZulipAPISheet.addCommand('', 'open-zulip-members', 'vd.push(ZulipMembersSheet("members", zulip_func=z_client.get_users, zulip_result_key="members"))', 'open list of all members')
234
+ ZulipAPISheet.addCommand('', 'open-zulip-streams', 'vd.push(vd.allStreams)', 'open list of all streams')
235
+ ZulipAPISheet.addCommand('', 'open-zulip-subs', 'vd.push(vd.subscribedStreams)', 'open list of subscribed streams')
236
+ ZulipAPISheet.addCommand('', 'open-zulip-msgs', 'vd.push(vd.allMessages)', 'open list of all messages')
237
+
238
+ ZulipMessagesSheet.addCommand('', 'reply-zulip-msg', 'reply_message(input(cursorRow["display_recipient"][1]["short_name"]+"> ", "message"), cursorRow)', 'reply to current topic')
239
+ ZulipMessagesSheet.addCommand('', 'edit-zulip-msg', 'update_message(cursorRow["id"], editCell(3, cursorRowIndex))', 'edit message content')
240
+
241
+ vd.addMenuItems('''
242
+ File > Zulip > profile > open-zulip-profile
243
+ File > Zulip > member list > open-zulip-members
244
+ File > Zulip > streams > open-zulip-streams
245
+ File > Zulip > subscriptions > open-zulip-subs
246
+ File > Zulip > messages > open-zulip-subs
247
+ File > Zulip > reply > reply-zulip-msg
248
+ File > Zulip > edit message > edit-zulip-msg
249
+ ''')
@@ -8,13 +8,23 @@ from visidata import vd, VisiData, asyncthread, Sheet, Progress, Menu, options
8
8
  from visidata import ColumnAttr, Column, Path
9
9
  from visidata.type_date import date
10
10
 
11
+ @VisiData.api
12
+ def guess_zip(vd, p):
13
+ if not p.is_url() and zipfile.is_zipfile(p.open_bytes()):
14
+ return dict(filetype='zip')
15
+
16
+ @VisiData.api
17
+ def guess_tar(vd, p):
18
+ if tarfile.is_tarfile(p.fp):
19
+ return dict(filetype='tar')
20
+
11
21
  @VisiData.api
12
22
  def open_zip(vd, p):
13
- return vd.ZipSheet(p.name, source=p)
23
+ return vd.ZipSheet(p.base_stem, source=p)
14
24
 
15
25
  @VisiData.api
16
26
  def open_tar(vd, p):
17
- return TarSheet(p.name, source=p)
27
+ return TarSheet(p.base_stem, source=p)
18
28
 
19
29
  VisiData.open_tgz = VisiData.open_tar
20
30
  VisiData.open_txz = VisiData.open_tar
@@ -37,16 +47,33 @@ class ZipSheet(Sheet):
37
47
  getter=lambda col, row: datetime.datetime(*row[0].date_time)),
38
48
  ]
39
49
  nKeys = 2
50
+ guide = '''# Zip Sheet
51
+ This is a list of files contained in the zipfile {sheet.displaySource}.
52
+
53
+ Once extracted, files can be loaded with `ENTER`.
54
+
55
+ Commands:
56
+
57
+ - `x` to extract current file to current directory
58
+ - `gx` to extract selected files to current directory
59
+ - `zx` to extract current file to a given pathname
60
+ - `gzx` to extract selected files to given directory
61
+
62
+ '''
40
63
 
41
64
  def openZipFile(self, fp, *args, **kwargs):
42
65
  '''Use VisiData input to handle password-protected zip files.'''
66
+ if isinstance(fp, zipfile.ZipFile):
67
+ zip_open = fp.open
68
+ elif isinstance(fp, unzip_http.RemoteZipFile):
69
+ zip_open = fp._open
43
70
  try:
44
- return fp.open(*args, **kwargs)
71
+ return zip_open(*args, **kwargs)
45
72
  except RuntimeError as err:
46
73
  if 'password required' in err.args[0]:
47
74
  pwd = vd.input(f'{args[0].filename} is encrypted, enter password: ', display=False)
48
- return fp.open(*args, **kwargs, pwd=pwd.encode('utf-8'))
49
- vd.error(err)
75
+ return zip_open(*args, **kwargs, pwd=pwd.encode('utf-8'))
76
+ vd.exceptionCaught(err)
50
77
 
51
78
  def openRow(self, row):
52
79
  fi, zpath = row
@@ -59,10 +86,16 @@ class ZipSheet(Sheet):
59
86
  files = []
60
87
  for row in rows:
61
88
  r, _ = row
62
- if (path/r.filename).exists():
63
- vd.confirm(f'{r.filename} exists, overwrite? ') #1452
89
+ vd.confirmOverwrite(path/r.filename) #1452
64
90
  self.extract_async(row)
65
91
 
92
+ def sysopen_row(self, row):
93
+ 'Extract file in row to tempdir and launch $EDITOR. Modifications will be discarded.'
94
+ import tempfile
95
+ with tempfile.TemporaryDirectory() as tempdir:
96
+ self.zfp.extract(member=row[0], path=tempdir)
97
+ vd.launchExternalEditorPath(Path(tempdir)/row[0].filename)
98
+
66
99
  @asyncthread
67
100
  def extract_async(self, *rows, path=None):
68
101
  'Extract rows to *path*, without confirmation.'
@@ -115,6 +148,7 @@ ZipSheet.addCommand('x', 'extract-file', 'extract(cursorRow)', 'extract current
115
148
  ZipSheet.addCommand('gx', 'extract-selected', 'extract(*onlySelectedRows)', 'extract selected files to current directory')
116
149
  ZipSheet.addCommand('zx', 'extract-file-to', 'extract(cursorRow, path=inputPath("extract to: "))', 'extract current file to given pathname')
117
150
  ZipSheet.addCommand('gzx', 'extract-selected-to', 'extract(*onlySelectedRows, path=inputPath("extract %d files to: " % nSelectedRows))', 'extract selected files to given directory')
151
+ ZipSheet.addCommand('Ctrl+O', 'sysopen-row', 'sysopen_row(cursorRow)', 'open $EDITOR with current file (modifications will be discarded)')
118
152
 
119
153
  vd.addMenu(Menu('File', Menu('Extract',
120
154
  Menu('current file', 'extract-file'),