nbcat 0.1.0__py3-none-any.whl → 0.8.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.
nbcat/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.8.1"
nbcat/enums.py ADDED
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CellType(str, Enum):
5
+ MARKDOWN = "markdown"
6
+ CODE = "code"
7
+ RAW = "raw"
8
+ HEADING = "heading"
9
+
10
+
11
+ class OutputType(str, Enum):
12
+ STREAM = "stream"
13
+ DISPLAY_DATA = "display_data"
14
+ EXECUTE_RESULT = "execute_result"
15
+ ERROR = "error"
16
+ PYOUT = "pyout"
nbcat/exceptions.py ADDED
@@ -0,0 +1,10 @@
1
+ class InvalidNotebookFormatError(Exception):
2
+ """Raised when the file is not a valid .ipynb or URL."""
3
+
4
+
5
+ class NotebookNotFoundError(Exception):
6
+ """Raised when the file or URL is not reachable."""
7
+
8
+
9
+ class UnsupportedNotebookTypeError(Exception):
10
+ """Raised when the file exists but is not a .ipynb document."""
nbcat/main.py ADDED
@@ -0,0 +1,168 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import requests
6
+ from pydantic import ValidationError
7
+ from rich import box
8
+ from rich.console import Console, RenderableType
9
+ from rich.markdown import Markdown
10
+ from rich.panel import Panel
11
+ from rich.syntax import Syntax
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+ from . import __version__
16
+ from .enums import CellType
17
+ from .exceptions import (
18
+ InvalidNotebookFormatError,
19
+ NotebookNotFoundError,
20
+ UnsupportedNotebookTypeError,
21
+ )
22
+ from .schemas import Cell, Notebook
23
+
24
+ console = Console()
25
+
26
+
27
+ def read_notebook(fp: str) -> Notebook:
28
+ """
29
+ Load and parse a Jupyter notebook from a local file or remote URL.
30
+
31
+ Args:
32
+ fp (str): Path to a local `.ipynb` file or a URL pointing to a notebook.
33
+
34
+ Returns
35
+ -------
36
+ Notebook: A validated Notebook instance parsed from JSON content.
37
+
38
+ Raises
39
+ ------
40
+ NotebookNotFoundError: If the file path or URL is unreachable.
41
+ UnsupportedNotebookTypeError: If the file exists but isn't a `.ipynb` file.
42
+ InvalidNotebookFormatError: If the file content is invalid JSON or doesn't match schema.
43
+ """
44
+ path = Path(fp)
45
+ if path.exists():
46
+ if path.suffix != ".ipynb":
47
+ raise UnsupportedNotebookTypeError(f"Unsupported file type: {path.suffix}")
48
+ content = path.read_text(encoding="utf-8")
49
+ elif fp.startswith("http://") or fp.startswith("https://"):
50
+ try:
51
+ with requests.Session() as req:
52
+ res = req.get(fp, timeout=5)
53
+ res.raise_for_status()
54
+ content = res.text
55
+ except requests.RequestException as e:
56
+ raise NotebookNotFoundError(f"Unable to fetch remote notebook: {e}")
57
+ else:
58
+ raise NotebookNotFoundError(f"Notebook not found: {fp}")
59
+ try:
60
+ return Notebook.model_validate_json(content)
61
+ except ValidationError as e:
62
+ raise InvalidNotebookFormatError(f"Invalid notebook: {e}")
63
+
64
+
65
+ def render_cell(cell: Cell) -> list[tuple[str | None, RenderableType]]:
66
+ """
67
+ Render the content of a notebook cell for display.
68
+
69
+ Depending on the cell type, the function returns a formatted object
70
+ that can be rendered in a terminal using the `rich` library.
71
+
72
+ Args:
73
+ cell (Cell): The notebook cell containing source content and type metadata.
74
+
75
+ Returns
76
+ -------
77
+ Markdown | Panel | Text | None: A Rich renderable for Markdown, Code, or Raw cells.
78
+ Returns None if the cell type is unrecognized or unsupported.
79
+ """
80
+
81
+ def _render_markdown(input: str) -> Markdown:
82
+ return Markdown(input)
83
+
84
+ def _render_code(input: str) -> Panel:
85
+ return Panel(Syntax(input, "python", line_numbers=True, theme="ansi_dark"), box=box.SQUARE)
86
+
87
+ def _render_raw(input: str) -> Text:
88
+ return Text(input)
89
+
90
+ def _render_heading(input: str) -> Text:
91
+ return Text(input)
92
+
93
+ RENDERERS = {
94
+ CellType.MARKDOWN: _render_markdown,
95
+ CellType.CODE: _render_code,
96
+ CellType.RAW: _render_raw,
97
+ CellType.HEADING: _render_heading,
98
+ }
99
+
100
+ rows: list[tuple[str | None, RenderableType]] = []
101
+ renderer = RENDERERS.get(cell.cell_type)
102
+ source = renderer(cell.input) if renderer else None
103
+ if source:
104
+ label = f"[green][{cell.execution_count}][/]:" if cell.execution_count else None
105
+ rows.append(
106
+ (
107
+ label,
108
+ source,
109
+ )
110
+ )
111
+
112
+ for o in cell.outputs:
113
+ if o.output:
114
+ label = f"[blue][{o.execution_count}][/]:" if o.execution_count else None
115
+ rows.append(
116
+ (
117
+ label,
118
+ o.output,
119
+ )
120
+ )
121
+ return rows
122
+
123
+
124
+ def print_notebook(nb: Notebook):
125
+ """
126
+ Print the notebook to the console with formatted cell inputs and outputs.
127
+
128
+ Args:
129
+ nb (Notebook): A Notebook object containing a list of cells.
130
+ """
131
+ if not nb.cells:
132
+ console.print("[bold red]Notebook contains no cells.")
133
+ return
134
+
135
+ layout = Table.grid(padding=1)
136
+ layout.add_column(no_wrap=True, width=6)
137
+ layout.add_column()
138
+
139
+ for cell in nb.cells:
140
+ for label, content in render_cell(cell):
141
+ layout.add_row(label, content)
142
+
143
+ console.print(layout)
144
+
145
+
146
+ def main():
147
+ parser = argparse.ArgumentParser(
148
+ description="cat for Jupyter Notebooks",
149
+ argument_default=argparse.SUPPRESS,
150
+ )
151
+ parser.add_argument("file", help="Path or URL to a .ipynb notebook", type=str)
152
+ parser.add_argument(
153
+ "--version",
154
+ help="print version information and quite",
155
+ action="version",
156
+ version=__version__,
157
+ )
158
+
159
+ try:
160
+ args = parser.parse_args()
161
+ notebook = read_notebook(args.file)
162
+ print_notebook(notebook)
163
+ except Exception as e:
164
+ sys.exit(f"nbcat: {e}")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
nbcat/schemas.py ADDED
@@ -0,0 +1,95 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, computed_field, model_validator
4
+
5
+ from .enums import CellType, OutputType
6
+ from .exceptions import InvalidNotebookFormatError
7
+
8
+
9
+ class BaseOutput(BaseModel):
10
+ output_type: OutputType
11
+ execution_count: int | None = None
12
+
13
+
14
+ class StreamOutput(BaseOutput):
15
+ name: str
16
+ text: list[str] | str
17
+
18
+ @computed_field
19
+ @property
20
+ def output(self) -> str:
21
+ if isinstance(self.text, list):
22
+ return "".join(self.text)
23
+ return self.text
24
+
25
+
26
+ class DisplayDataOutput(BaseOutput):
27
+ data: dict[str, Any]
28
+
29
+ @computed_field
30
+ @property
31
+ def output(self) -> str:
32
+ # TODO: add support for rich display outputs
33
+ return ""
34
+
35
+
36
+ class ErrorOutput(BaseOutput):
37
+ ename: str
38
+ evalue: str
39
+ traceback: list[str]
40
+
41
+ @computed_field
42
+ @property
43
+ def output(self) -> str:
44
+ return "\n".join(self.traceback)
45
+
46
+
47
+ class PyoutDataOutput(BaseOutput):
48
+ text: list[str]
49
+
50
+ @computed_field
51
+ @property
52
+ def output(self) -> str:
53
+ return "\n".join(self.text)
54
+
55
+
56
+ class Cell(BaseModel):
57
+ cell_type: CellType
58
+ source: list[str] | str
59
+ level: int | None = None
60
+ execution_count: int | None = None
61
+ outputs: list[StreamOutput | DisplayDataOutput | ErrorOutput | PyoutDataOutput] = []
62
+
63
+ @model_validator(mode="before")
64
+ @classmethod
65
+ def handle_format_versions(cls, data: dict[str, Any]) -> dict[str, Any]:
66
+ if data.get("input"):
67
+ data["source"] = data["input"]
68
+ return data
69
+
70
+ @computed_field
71
+ @property
72
+ def input(self) -> str:
73
+ if self.cell_type == CellType.HEADING and self.level is not None:
74
+ return f"{'#' * self.level} {self.source}"
75
+
76
+ if isinstance(self.source, list):
77
+ return "".join(self.source)
78
+
79
+ return self.source
80
+
81
+
82
+ class Notebook(BaseModel):
83
+ cells: list[Cell] = []
84
+ nbformat: int
85
+
86
+ @model_validator(mode="before")
87
+ @classmethod
88
+ def handle_format_versions(cls, data: dict[str, Any]) -> dict[str, Any]:
89
+ if data.get("worksheets"):
90
+ try:
91
+ data["cells"] = data.get("worksheets")[0].get("cells", [])
92
+ except (KeyError, IndexError, TypeError) as e:
93
+ print(e)
94
+ raise InvalidNotebookFormatError(f"Invalid v3 notebook structure: {e}")
95
+ return data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbcat
3
- Version: 0.1.0
3
+ Version: 0.8.1
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
@@ -29,15 +29,14 @@ License: MIT License
29
29
  SOFTWARE.
30
30
  License-File: LICENSE
31
31
  Requires-Python: >=3.10
32
- Requires-Dist: aiohttp
33
32
  Requires-Dist: pydantic
33
+ Requires-Dist: requests
34
34
  Requires-Dist: rich
35
35
  Provides-Extra: dev
36
- Requires-Dist: aioresponses; extra == 'dev'
37
36
  Requires-Dist: pytest; extra == 'dev'
38
- Requires-Dist: pytest-asyncio; extra == 'dev'
39
37
  Requires-Dist: pytest-cov; extra == 'dev'
40
38
  Requires-Dist: pytest-mock; extra == 'dev'
39
+ Requires-Dist: pytest-responses; extra == 'dev'
41
40
  Requires-Dist: ruff; extra == 'dev'
42
41
  Description-Content-Type: text/markdown
43
42
 
@@ -0,0 +1,11 @@
1
+ nbcat/__init__.py,sha256=Ocl79hbbH8_jdr5dGC90VR1cAvZc05Rc0tkZttUnMjo,22
2
+ nbcat/enums.py,sha256=ZsuOwYLF0D4PVwSkS74LwoXY0y0DkeBToLBWnmiS97Y,300
3
+ nbcat/exceptions.py,sha256=Ho7LQz9K70VtIMDNtAwuAtGmb-lFKxGxSj7MN3-EpDA,321
4
+ nbcat/main.py,sha256=5OawByiWMAX6LX25-Net8zZDE4_0m7a8GOKMIN-UjJc,4919
5
+ nbcat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ nbcat/schemas.py,sha256=vRQPY9n2QnjIvtuaj1Rb6Tn0CdS9Av255IjXs3VSQpc,2354
7
+ nbcat-0.8.1.dist-info/METADATA,sha256=rJP4LdPGhVehaF6b7xi3teLuxv-zi9-IUTnX7DeF-3I,3385
8
+ nbcat-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ nbcat-0.8.1.dist-info/entry_points.txt,sha256=io_GRDsecAkYuCZALsjyea3VBq91VCoSznqlZEAJshY,42
10
+ nbcat-0.8.1.dist-info/licenses/LICENSE,sha256=7GjUnahXdd5opdvlpJdb1BisLbiXt2iOFhzIUduhdkE,1072
11
+ nbcat-0.8.1.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nbcat = nbcat.main:main
nbcat/cli.py DELETED
@@ -1,73 +0,0 @@
1
- import argparse
2
- import asyncio
3
- import sys
4
-
5
- from pydantic import ValidationError
6
-
7
- from . import __version__
8
- from .settings import Settings
9
-
10
-
11
- async def do_sample_command_1():
12
- """Run used defined command 1."""
13
-
14
-
15
- async def do_sample_command_2():
16
- """Run user defined command 2."""
17
-
18
-
19
- def main():
20
- parser = argparse.ArgumentParser(
21
- description="cat for jupyter notebooks",
22
- argument_default=argparse.SUPPRESS,
23
- )
24
- parser.add_argument(
25
- "--list_field", help="Example of multi value list separated by comma.", type=str
26
- )
27
- parser.add_argument(
28
- "--version",
29
- help="Print version information and quite",
30
- action="version",
31
- version=__version__,
32
- )
33
-
34
- # Commands
35
- commands = parser.add_subparsers(title="Commands", dest="command")
36
-
37
- # Sample command 1
38
- sample_command_1 = commands.add_parser(
39
- "sample_command_1",
40
- help="Sample command description.",
41
- argument_default=argparse.SUPPRESS,
42
- )
43
-
44
- sample_command_1.set_defaults(func=do_sample_command_1)
45
-
46
- # Sample command 2
47
- sample_command_2 = commands.add_parser(
48
- "sample_command_2",
49
- help="Sample command description.",
50
- argument_default=argparse.SUPPRESS,
51
- )
52
-
53
- sample_command_2.set_defaults(func=do_sample_command_2)
54
-
55
- try:
56
- args = parser.parse_args()
57
- settings = Settings(**vars(args))
58
- except ValidationError as e:
59
- error = e.errors(include_url=False, include_context=False)[0]
60
- sys.exit(
61
- "Wrong argument value passed ({}): {}".format(
62
- error.get("loc", ("system",))[0], error.get("msg")
63
- )
64
- )
65
-
66
- if args.command:
67
- asyncio.run(args.func(settings))
68
- else:
69
- parser.print_help()
70
-
71
-
72
- if __name__ == "__main__":
73
- main()
nbcat/nbcat.py DELETED
@@ -1,22 +0,0 @@
1
- from datetime import datetime
2
-
3
- import aiohttp
4
-
5
-
6
- async def my_func(name) -> str:
7
- """Get message from remote server."""
8
- async with aiohttp.ClientSession() as session:
9
- async with session.get("https://example.com/api.json", params={"name": name}) as response:
10
- data = await response.json()
11
- return data["message"]
12
-
13
-
14
- def now() -> datetime:
15
- """Ready to mock method for date extraction."""
16
- return datetime.now()
17
-
18
-
19
- def get_time() -> str:
20
- """Return today's date."""
21
- date = now()
22
- return f"Today is {date.strftime('%A, %B %d, %Y')}"
nbcat/settings.py DELETED
@@ -1,16 +0,0 @@
1
- from pydantic import BaseModel, model_validator
2
-
3
-
4
- class Settings(BaseModel):
5
- """Validates cli arguments."""
6
-
7
- field: str | None = None
8
- list_field: list[str] = []
9
- bool_field: bool = False
10
-
11
- @model_validator(mode="before")
12
- def parse_list_field(values: dict):
13
- """You can pass multiple values as a comma separated string."""
14
- if isinstance(values.get("list_field"), str):
15
- values["list_field"] = values.get("list_field").split(",")
16
- return values
@@ -1,10 +0,0 @@
1
- nbcat/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
- nbcat/cli.py,sha256=BeijbCAe1J3s7UgeKW_hdRdXvkg1jo6XaT4aPGKz-Yc,1802
3
- nbcat/nbcat.py,sha256=eWgwOGCnOK6xQ1qC8Ku2vmQgPvA5Nx6mfdM8Ca3vU6w,580
4
- nbcat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- nbcat/settings.py,sha256=I6sGyVidPjW_76zq9mefnS2-vzRUKL87jSPluPe_SwQ,498
6
- nbcat-0.1.0.dist-info/METADATA,sha256=Woo2Z2dx6A8vMsXE4k0cpYuTJ_llb1UVOqG8ENoGitE,3426
7
- nbcat-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- nbcat-0.1.0.dist-info/entry_points.txt,sha256=a_byTgZyR_et6CxNcKCQlw1EHisUaIFRBRLh0tUQs2U,41
9
- nbcat-0.1.0.dist-info/licenses/LICENSE,sha256=7GjUnahXdd5opdvlpJdb1BisLbiXt2iOFhzIUduhdkE,1072
10
- nbcat-0.1.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- nbcat = nbcat.cli:main
File without changes