nbcat 0.1.0__py3-none-any.whl → 0.8.2__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 +1 -1
- nbcat/enums.py +16 -0
- nbcat/exceptions.py +10 -0
- nbcat/main.py +168 -0
- nbcat/schemas.py +94 -0
- {nbcat-0.1.0.dist-info → nbcat-0.8.2.dist-info}/METADATA +3 -4
- nbcat-0.8.2.dist-info/RECORD +11 -0
- nbcat-0.8.2.dist-info/entry_points.txt +2 -0
- nbcat/cli.py +0 -73
- nbcat/nbcat.py +0 -22
- nbcat/settings.py +0 -16
- nbcat-0.1.0.dist-info/RECORD +0 -10
- nbcat-0.1.0.dist-info/entry_points.txt +0 -2
- {nbcat-0.1.0.dist-info → nbcat-0.8.2.dist-info}/WHEEL +0 -0
- {nbcat-0.1.0.dist-info → nbcat-0.8.2.dist-info}/licenses/LICENSE +0 -0
nbcat/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.8.2"
|
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,94 @@
|
|
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
|
+
text: list[str] | str
|
16
|
+
|
17
|
+
@computed_field
|
18
|
+
@property
|
19
|
+
def output(self) -> str:
|
20
|
+
if isinstance(self.text, list):
|
21
|
+
return "".join(self.text)
|
22
|
+
return self.text
|
23
|
+
|
24
|
+
|
25
|
+
class DisplayDataOutput(BaseOutput):
|
26
|
+
data: dict[str, Any]
|
27
|
+
|
28
|
+
@computed_field
|
29
|
+
@property
|
30
|
+
def output(self) -> str:
|
31
|
+
# TODO: add support for rich display outputs
|
32
|
+
return ""
|
33
|
+
|
34
|
+
|
35
|
+
class ErrorOutput(BaseOutput):
|
36
|
+
ename: str
|
37
|
+
evalue: str
|
38
|
+
traceback: list[str]
|
39
|
+
|
40
|
+
@computed_field
|
41
|
+
@property
|
42
|
+
def output(self) -> str:
|
43
|
+
return "\n".join(self.traceback)
|
44
|
+
|
45
|
+
|
46
|
+
class PyoutDataOutput(BaseOutput):
|
47
|
+
text: list[str]
|
48
|
+
|
49
|
+
@computed_field
|
50
|
+
@property
|
51
|
+
def output(self) -> str:
|
52
|
+
return "\n".join(self.text)
|
53
|
+
|
54
|
+
|
55
|
+
class Cell(BaseModel):
|
56
|
+
cell_type: CellType
|
57
|
+
source: list[str] | str
|
58
|
+
level: int | None = None
|
59
|
+
execution_count: int | None = None
|
60
|
+
outputs: list[StreamOutput | DisplayDataOutput | ErrorOutput | PyoutDataOutput] = []
|
61
|
+
|
62
|
+
@model_validator(mode="before")
|
63
|
+
@classmethod
|
64
|
+
def handle_format_versions(cls, data: dict[str, Any]) -> dict[str, Any]:
|
65
|
+
if data.get("input"):
|
66
|
+
data["source"] = data["input"]
|
67
|
+
return data
|
68
|
+
|
69
|
+
@computed_field
|
70
|
+
@property
|
71
|
+
def input(self) -> str:
|
72
|
+
if self.cell_type == CellType.HEADING and self.level is not None:
|
73
|
+
return f"{'#' * self.level} {self.source}"
|
74
|
+
|
75
|
+
if isinstance(self.source, list):
|
76
|
+
return "".join(self.source)
|
77
|
+
|
78
|
+
return self.source
|
79
|
+
|
80
|
+
|
81
|
+
class Notebook(BaseModel):
|
82
|
+
cells: list[Cell] = []
|
83
|
+
nbformat: int
|
84
|
+
|
85
|
+
@model_validator(mode="before")
|
86
|
+
@classmethod
|
87
|
+
def handle_format_versions(cls, data: dict[str, Any]) -> dict[str, Any]:
|
88
|
+
if data.get("worksheets"):
|
89
|
+
try:
|
90
|
+
data["cells"] = data.get("worksheets")[0].get("cells", [])
|
91
|
+
except (KeyError, IndexError, TypeError) as e:
|
92
|
+
print(e)
|
93
|
+
raise InvalidNotebookFormatError(f"Invalid v3 notebook structure: {e}")
|
94
|
+
return data
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nbcat
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.2
|
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=B7GiO0rd49YwtLYjvPg4lmCZEDlMTonslQKdSImaMJk,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=F5Wl0ZWa5j8RK4H98nDJm69h3oecEuP6wu62823Cxyg,2340
|
7
|
+
nbcat-0.8.2.dist-info/METADATA,sha256=4yOzFXgN_yzTC0X8ItssABMyzvNNPVSp1KBXBeb-hi8,3385
|
8
|
+
nbcat-0.8.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
+
nbcat-0.8.2.dist-info/entry_points.txt,sha256=io_GRDsecAkYuCZALsjyea3VBq91VCoSznqlZEAJshY,42
|
10
|
+
nbcat-0.8.2.dist-info/licenses/LICENSE,sha256=7GjUnahXdd5opdvlpJdb1BisLbiXt2iOFhzIUduhdkE,1072
|
11
|
+
nbcat-0.8.2.dist-info/RECORD,,
|
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
|
nbcat-0.1.0.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|