main
py 184 lines 6.95 KB
Raw
1 # Copyright 2026 Google LLC
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import nbformat
16 from typing import Any, Dict, List
17 import json
18 import os
19 import uuid
20
21
22 def export_history(events: List[Dict[str, Any]], session_name: str, output_path: str):
23 """
24 Exports history based on file extension.
25 """
26 ext = os.path.splitext(output_path)[1].lower()
27
28 if ext == ".ipynb":
29 nb = convert_history_to_ipynb(events, session_name)
30 with open(output_path, "w", encoding="utf-8") as f:
31 nbformat.write(nb, f)
32
33 elif ext == ".jsonl":
34 with open(output_path, "w", encoding="utf-8") as f:
35 for event in events:
36 f.write(json.dumps(event) + "\n")
37
38 elif ext == ".md":
39 with open(output_path, "w", encoding="utf-8") as f:
40 f.write(f"# Colab Session: {session_name}\n\n")
41 for event in events:
42 ts = event.get("timestamp", "").split(".")[0].replace("T", " ")
43 etype = event.get("event_type")
44 if etype == "execution":
45 code = event.get("code", "")
46 f.write(f"### Execution ({ts})\n```python\n{code}\n```\n\n")
47 for o in event.get("outputs", []):
48 if "text" in o:
49 f.write(f"**Output**:\n```\n{o['text']}```\n\n")
50 elif etype == "session_created":
51 f.write(
52 f"## Session Created: {ts}\n- Endpoint: `{event.get('endpoint')}`\n\n"
53 )
54 elif etype == "file_operation":
55 f.write(
56 f"*File Operation*: `{event.get('op')}` on `{event.get('path', event.get('remote', ''))}`\n\n"
57 )
58
59 elif ext == ".txt":
60 with open(output_path, "w", encoding="utf-8") as f:
61 f.write(f"Colab Session: {session_name}\n" + "=" * 20 + "\n\n")
62 for event in events:
63 ts = event.get("timestamp", "").split(".")[0].replace("T", " ")
64 etype = event.get("event_type", "unknown")
65 f.write(f"[{ts}] {etype.upper()}: ")
66 if etype == "execution":
67 f.write(event.get("code", "").strip() + "\n")
68 else:
69 f.write(str(event) + "\n")
70
71 else:
72 print(f"[colab] Unsupported export format: {ext}")
73 return
74
75 print(f"[colab] Exported history to '{output_path}'.")
76
77
78 def convert_history_to_ipynb(
79 events: List[Dict[str, Any]], session_name: str
80 ) -> nbformat.NotebookNode:
81 """
82 Converts a list of session events to a Jupyter Notebook (v4).
83 """
84 nb = nbformat.v4.new_notebook()
85 nb.metadata.kernelspec = {
86 "display_name": "Python 3 (Google Colab)",
87 "language": "python",
88 "name": "python3",
89 }
90
91 title = f"# Colab Session: {session_name}\nGenerated from colab-cli history log."
92 cell = nbformat.v4.new_markdown_cell(title)
93 cell.id = str(uuid.uuid4())
94 nb.cells.append(cell)
95
96 for event in events:
97 etype = event.get("event_type")
98 ts = event.get("timestamp", "").split(".")[0].replace("T", " ")
99
100 if etype == "session_created":
101 meta = f"**Session Created**: {ts}\n- Endpoint: `{event.get('endpoint')}`\n- Hardware: `{event.get('accelerator')}`"
102 cell = nbformat.v4.new_markdown_cell(meta)
103 cell.id = str(uuid.uuid4())
104 nb.cells.append(cell)
105
106 elif etype == "execution":
107 code = event.get("code", "")
108 # Check for shell commands (starting with ! or from piped console)
109 # If it's a raw shell command from a console pipe, wrap it in %%bash if it doesn't have !
110 if event.get("source") == "piped" and not code.startswith("!"):
111 code = "%%bash\n" + code
112
113 outputs = _map_outputs(event.get("outputs", []))
114 cell = nbformat.v4.new_code_cell(code, outputs=outputs)
115 cell.id = str(uuid.uuid4())
116 nb.cells.append(cell)
117
118 elif etype == "automation":
119 op = event.get("op")
120 code = event.get("code", "")
121 cell = nbformat.v4.new_markdown_cell(f"### Automation: {op} ({ts})")
122 cell.id = str(uuid.uuid4())
123 nb.cells.append(cell)
124 if code:
125 # Get the result from the next event if it's automation_result
126 cell = nbformat.v4.new_code_cell(code)
127 cell.id = str(uuid.uuid4())
128 nb.cells.append(cell)
129
130 elif etype == "automation_result":
131 # We can attach these outputs to the previous automation cell if we were more clever,
132 # but for now let's just ensure we capture the output.
133 if event.get("outputs"):
134 cell = nbformat.v4.new_code_cell(
135 "# Result of previous automation",
136 outputs=_map_outputs(event.get("outputs")),
137 )
138 cell.id = str(uuid.uuid4())
139 nb.cells.append(cell)
140
141 elif etype == "file_operation":
142 cell = nbformat.v4.new_markdown_cell(
143 f"*File Operation*: `{event.get('op')}` on `{event.get('path', event.get('remote', ''))}`"
144 )
145 cell.id = str(uuid.uuid4())
146 nb.cells.append(cell)
147
148 elif etype == "stdin_request":
149 cell = nbformat.v4.new_markdown_cell(
150 f"> **Input Requested**: {event.get('prompt')}"
151 )
152 cell.id = str(uuid.uuid4())
153 nb.cells.append(cell)
154
155 elif etype == "input_reply":
156 cell = nbformat.v4.new_markdown_cell(
157 f"> **User Input**: `{event.get('value')}`"
158 )
159 cell.id = str(uuid.uuid4())
160 nb.cells.append(cell)
161
162 return nb
163
164
165 def _map_outputs(outputs: List[Dict[str, Any]]) -> List[nbformat.NotebookNode]:
166 nb_outputs = []
167 for o in outputs:
168 otype = o.get("output_type")
169 if "text" in o:
170 nb_outputs.append(
171 nbformat.v4.new_output("stream", name="stdout", text=o["text"])
172 )
173 elif "data" in o:
174 nb_outputs.append(nbformat.v4.new_output("display_data", data=o["data"]))
175 elif otype == "error":
176 nb_outputs.append(
177 nbformat.v4.new_output(
178 "error",
179 ename=o.get("ename", "Error"),
180 evalue=o.get("evalue", ""),
181 traceback=o.get("traceback", []),
182 )
183 )
184 return nb_outputs