nbcat 0.12.0__py3-none-any.whl → 0.13.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.
nbcat/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.12.0"
1
+ __version__ = "0.13.0"
nbcat/main.py CHANGED
@@ -6,7 +6,6 @@ import argcomplete
6
6
  import requests
7
7
  from argcomplete.completers import FilesCompleter
8
8
  from pydantic import ValidationError
9
- from rich import box
10
9
  from rich.console import Console, Group, RenderableType
11
10
  from rich.padding import Padding
12
11
  from rich.panel import Panel
@@ -23,6 +22,7 @@ from .exceptions import (
23
22
  )
24
23
  from .image import Image
25
24
  from .markdown import Markdown
25
+ from .pager import Pager
26
26
  from .schemas import Cell, Notebook
27
27
 
28
28
  console = Console()
@@ -87,10 +87,8 @@ def render_cell(cell: Cell) -> RenderableType:
87
87
  def _render_markdown(input: str) -> Markdown:
88
88
  return Markdown(input, code_theme="ansi_dark")
89
89
 
90
- def _render_code(input: str, language: str = "python") -> Panel:
91
- return Panel(
92
- Syntax(input, language, line_numbers=True, theme="ansi_dark", dedent=True), padding=0
93
- )
90
+ def _render_code(input: str, language: str = "python") -> Syntax:
91
+ return Syntax(input, language, theme="ansi_dark", padding=(1, 2), dedent=True)
94
92
 
95
93
  def _render_raw(input: str) -> Text:
96
94
  return Text(input)
@@ -116,41 +114,48 @@ def render_cell(cell: Cell) -> RenderableType:
116
114
  renderer = RENDERERS.get(cell.cell_type)
117
115
  source = renderer(cell.input) if renderer else None
118
116
  if source:
119
- rows.append(Padding(source, (1, 0)))
117
+ s_title = f"[green]In [{cell.execution_count}][/]" if cell.execution_count else None
118
+ if s_title:
119
+ rows.append(Panel(source, title=s_title, title_align="left"))
120
+ else:
121
+ rows.append(Padding(source, (1, 0)))
122
+
120
123
  if not cell.outputs:
121
- return source
124
+ return rows.pop()
122
125
 
123
126
  for o in cell.outputs:
127
+ o_title = f"[blue]Out [{o.execution_count}][/]" if o.execution_count else None
124
128
  if o.output:
125
129
  renderer = RENDERERS.get(o.output.output_type)
126
130
  output = renderer(o.output.text) if renderer else None
127
131
  if output:
128
- rows.append(Panel(output, style="italic", box=box.MINIMAL))
132
+ if o_title:
133
+ rows.append(Panel(output, style="italic", title=o_title, title_align="left"))
134
+ else:
135
+ rows.append(Padding(output, (1, 0), style="italic"))
129
136
  return Group(*rows)
130
137
 
131
138
 
132
- def print_notebook(nb: Notebook):
139
+ def render_notebook(nb: Notebook) -> list[RenderableType]:
133
140
  """
134
- Print the notebook to the console with formatted cell inputs and outputs.
141
+ Convert a Notebook object into a list of rich renderables for terminal display.
142
+
143
+ Each cell in the notebook is processed and rendered using `render_cell`,
144
+ producing a sequence of styled input/output blocks suitable for use in a
145
+ Textual or Rich-based terminal UI.
135
146
 
136
147
  Args:
137
- nb (Notebook): A Notebook object containing a list of cells.
138
- """
139
- if not nb.cells:
140
- console.print("[bold red]Notebook contains no cells.")
141
- return
148
+ nb (Notebook): The notebook object containing parsed cells (e.g., from a .ipynb file).
142
149
 
150
+ Returns
151
+ -------
152
+ list[RenderableType]: A list of rich renderable objects representing the notebook cells.
153
+ Returns an empty list if the notebook has no cells.
154
+ """
155
+ rendered: list[RenderableType] = []
143
156
  for cell in nb.cells:
144
- rendered = render_cell(cell)
145
- if isinstance(rendered, Group):
146
- out = Panel(
147
- rendered,
148
- title=f"[green][{cell.execution_count}][/]" if cell.execution_count else None,
149
- title_align="left",
150
- )
151
- else:
152
- out = Padding(rendered, (1, 0))
153
- console.print(out)
157
+ rendered.append(render_cell(cell))
158
+ return rendered
154
159
 
155
160
 
156
161
  def main():
@@ -162,20 +167,38 @@ def main():
162
167
  "file", help="Path or URL to a .ipynb notebook", type=str
163
168
  ).completer = FilesCompleter()
164
169
  parser.add_argument(
170
+ "-v",
165
171
  "--version",
166
172
  help="print version information and quite",
167
173
  action="version",
168
174
  version=__version__,
169
175
  )
170
176
  parser.add_argument(
171
- "--debug", help="enable extended error output", action="store_true", default=False
177
+ "-d", "--debug", help="enable extended error output", action="store_true", default=False
178
+ )
179
+ parser.add_argument(
180
+ "-p",
181
+ "--page",
182
+ help="enable paginated view mode (similar to less)",
183
+ action="store_true",
184
+ default=False,
172
185
  )
173
186
 
174
187
  try:
175
188
  argcomplete.autocomplete(parser)
176
189
  args = parser.parse_args()
190
+
177
191
  notebook = read_notebook(args.file, debug=args.debug)
178
- print_notebook(notebook)
192
+ objects = render_notebook(notebook)
193
+
194
+ if not objects:
195
+ console.print("[bold red]Notebook contains no cells.")
196
+ return
197
+
198
+ if args.page:
199
+ Pager(objects).run()
200
+ else:
201
+ console.print(*objects)
179
202
  except Exception as e:
180
203
  sys.exit(f"nbcat: {e}")
181
204
 
nbcat/pager.py ADDED
@@ -0,0 +1,82 @@
1
+ from rich.console import RenderableType
2
+ from textual.app import App, ComposeResult
3
+ from textual.binding import Binding
4
+ from textual.containers import VerticalScroll
5
+ from textual.widgets import Static
6
+
7
+
8
+ class Pager(App):
9
+ BINDINGS = [
10
+ # Exit
11
+ Binding("q", "quit", "Quit"),
12
+ Binding(":q", "quit", "Quit"),
13
+ Binding("Q", "quit", "Quit"),
14
+ Binding(":Q", "quit", "Quit"),
15
+ Binding("ZZ", "quit", "Quit"),
16
+ # One line
17
+ Binding("j", "scroll_down", "Down"),
18
+ Binding("e", "scroll_down", "Down"),
19
+ Binding("^e", "scroll_down", "Down"),
20
+ Binding("^n", "scroll_down", "Down"),
21
+ Binding("cr", "scroll_down", "Down"), # carriage return = Enter
22
+ Binding("k", "scroll_up", "Up"),
23
+ Binding("y", "scroll_up", "Up"),
24
+ Binding("ctrl+y", "scroll_up", "Up"),
25
+ Binding("ctrl+k", "scroll_up", "Up"),
26
+ Binding("ctrl+p", "scroll_up", "Up"),
27
+ # One window
28
+ Binding("f", "page_down", "Page Down"),
29
+ Binding("ctrl+f", "page_down", "Page Down"),
30
+ Binding("ctrl+v", "page_down", "Page Down"),
31
+ Binding("space", "page_down", "Page Down"),
32
+ Binding("b", "page_up", "Page Up"),
33
+ Binding("ctrl+b", "page_up", "Page Up"),
34
+ Binding("escape+v", "page_up", "Page Up"),
35
+ # Extended window control
36
+ Binding("z", "page_down", "Window Down (set N)"),
37
+ Binding("w", "page_up", "Window Up (set N)"),
38
+ Binding("escape+space", "page_down", "Forward one window, no EOF stop"),
39
+ Binding("d", "half_page_down", "Half Page Down"),
40
+ Binding("ctrl+d", "half_page_down", "Half Page Down"),
41
+ # Jumping
42
+ Binding("g", "go_to_top", "Top of File"),
43
+ Binding("<", "go_to_top", "Top of File"),
44
+ Binding("escape+<", "go_to_top", "Top of File"),
45
+ Binding("G", "go_to_bottom", "Bottom of File"),
46
+ Binding(">", "go_to_bottom", "Bottom of File"),
47
+ Binding("escape+>", "go_to_bottom", "Bottom of File"),
48
+ ]
49
+
50
+ def __init__(self, objects: list[RenderableType]):
51
+ super().__init__()
52
+ self._objects = objects
53
+
54
+ def compose(self) -> ComposeResult:
55
+ with VerticalScroll():
56
+ for obj in self._objects:
57
+ yield Static(obj)
58
+
59
+ def on_mount(self) -> None:
60
+ self.theme = "textual-ansi"
61
+ self.viewer = self.query_one(VerticalScroll)
62
+
63
+ def action_scroll_down(self) -> None:
64
+ self.viewer.scroll_to(y=self.viewer.scroll_y + 1)
65
+
66
+ def action_scroll_up(self) -> None:
67
+ self.viewer.scroll_to(y=self.viewer.scroll_y - 1)
68
+
69
+ def action_page_down(self) -> None:
70
+ self.viewer.scroll_to(y=self.viewer.scroll_y + self.viewer.virtual_size.height)
71
+
72
+ def action_page_up(self) -> None:
73
+ self.viewer.scroll_to(y=max(self.viewer.scroll_y - self.viewer.virtual_size.height, 0))
74
+
75
+ def action_half_page_down(self) -> None:
76
+ self.viewer.scroll_to(y=self.viewer.scroll_y + (self.viewer.virtual_size.height / 2))
77
+
78
+ def action_go_to_top(self) -> None:
79
+ self.viewer.scroll_to(y=0)
80
+
81
+ def action_go_to_bottom(self) -> None:
82
+ self.viewer.scroll_to(y=self.viewer.virtual_size.height)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbcat
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: cat for jupyter notebooks
5
5
  Project-URL: Homepage, https://github.com/akopdev/nbcat
6
6
  Project-URL: Repository, https://github.com/akopdev/nbcat
@@ -33,6 +33,7 @@ Requires-Dist: argcomplete
33
33
  Requires-Dist: pydantic
34
34
  Requires-Dist: requests
35
35
  Requires-Dist: rich
36
+ Requires-Dist: textual
36
37
  Requires-Dist: timg
37
38
  Provides-Extra: dev
38
39
  Requires-Dist: pytest; extra == 'dev'
@@ -52,9 +53,10 @@ Description-Content-Type: text/markdown
52
53
 
53
54
  ## Features
54
55
 
55
- - Very fast and lightweight with minimal dependencies
56
- - Preview remote notebooks without downloading them
57
- - Supports for all Jupyter notebook versions, including old legacy formats
56
+ - Very fast and lightweight with minimal dependencies.
57
+ - Preview remote notebooks without downloading them.
58
+ - Enable paginated view mode with keyboard navigation (similar to `less`).
59
+ - Supports for all Jupyter notebook versions, including old legacy formats.
58
60
 
59
61
  ## Motivation
60
62
 
@@ -0,0 +1,14 @@
1
+ nbcat/__init__.py,sha256=DgpLNbv0e1LIEOOe54Db8_390i9pelMEFEnsBsNmyhA,23
2
+ nbcat/enums.py,sha256=Fn8PIcLl_uY4nQIs1EUvmKTwfhNUIZgmhRFiCSJk9wk,411
3
+ nbcat/exceptions.py,sha256=Ho7LQz9K70VtIMDNtAwuAtGmb-lFKxGxSj7MN3-EpDA,321
4
+ nbcat/image.py,sha256=hGZZK5mtij7ckWAvQwzim3thqGC92pLW5ZU5CYbRv_Q,995
5
+ nbcat/main.py,sha256=FSXD7SobpjhwDvOKAS1C-Lntmv9mPJhDUugjq8dBzAA,6678
6
+ nbcat/markdown.py,sha256=GkPNfzBhNLMl89pVcj14U6ytYUVk3yNB0G1zGYRRJ04,2962
7
+ nbcat/pager.py,sha256=Yu0XIh5MvhvT-cmULYVxo6s2Ufjf2CQ-NDS5fcMQ-IM,3184
8
+ nbcat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ nbcat/schemas.py,sha256=a7GoKgPTHgun199-J-sZq-ahkdQwvyRaCdQVg1gC798,3135
10
+ nbcat-0.13.0.dist-info/METADATA,sha256=rspE5gM8qznr1ZNL9REUdPBJho1QRlDRcXSTl2ngSH8,4265
11
+ nbcat-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ nbcat-0.13.0.dist-info/entry_points.txt,sha256=io_GRDsecAkYuCZALsjyea3VBq91VCoSznqlZEAJshY,42
13
+ nbcat-0.13.0.dist-info/licenses/LICENSE,sha256=7GjUnahXdd5opdvlpJdb1BisLbiXt2iOFhzIUduhdkE,1072
14
+ nbcat-0.13.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- nbcat/__init__.py,sha256=eHjt9DPsMbptabS2yGx9Yhbyzq5hFSUHXb7zc8Q_8-o,23
2
- nbcat/enums.py,sha256=Fn8PIcLl_uY4nQIs1EUvmKTwfhNUIZgmhRFiCSJk9wk,411
3
- nbcat/exceptions.py,sha256=Ho7LQz9K70VtIMDNtAwuAtGmb-lFKxGxSj7MN3-EpDA,321
4
- nbcat/image.py,sha256=hGZZK5mtij7ckWAvQwzim3thqGC92pLW5ZU5CYbRv_Q,995
5
- nbcat/main.py,sha256=vchFzwcvPGmv4pg5AHRaOOsZh83ynmxkTqIKl0z_Sys,5749
6
- nbcat/markdown.py,sha256=GkPNfzBhNLMl89pVcj14U6ytYUVk3yNB0G1zGYRRJ04,2962
7
- nbcat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- nbcat/schemas.py,sha256=a7GoKgPTHgun199-J-sZq-ahkdQwvyRaCdQVg1gC798,3135
9
- nbcat-0.12.0.dist-info/METADATA,sha256=QOcOghWxUMkPA986f-xscLwz-YQrbZQPUJQF8P3r4cQ,4164
10
- nbcat-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- nbcat-0.12.0.dist-info/entry_points.txt,sha256=io_GRDsecAkYuCZALsjyea3VBq91VCoSznqlZEAJshY,42
12
- nbcat-0.12.0.dist-info/licenses/LICENSE,sha256=7GjUnahXdd5opdvlpJdb1BisLbiXt2iOFhzIUduhdkE,1072
13
- nbcat-0.12.0.dist-info/RECORD,,
File without changes