convoviz 0.2.8__py3-none-any.whl → 0.2.10__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.
@@ -139,7 +139,8 @@ def generate_wordclouds(
139
139
  text = group.plaintext("user", "assistant")
140
140
  if text.strip():
141
141
  img = generate_wordcloud(text, config)
142
- img.save(output_dir / f"{week.strftime('%Y week %W')}.png", optimize=True)
142
+ # Format: 2024-W15.png (ISO week format)
143
+ img.save(output_dir / f"{week.strftime('%Y-W%W')}.png", optimize=True)
143
144
 
144
145
  for month, group in tqdm(
145
146
  month_groups.items(),
@@ -149,7 +150,8 @@ def generate_wordclouds(
149
150
  text = group.plaintext("user", "assistant")
150
151
  if text.strip():
151
152
  img = generate_wordcloud(text, config)
152
- img.save(output_dir / f"{month.strftime('%Y %B')}.png", optimize=True)
153
+ # Format: 2024-03-March.png (consistent with folder naming)
154
+ img.save(output_dir / f"{month.strftime('%Y-%m-%B')}.png", optimize=True)
153
155
 
154
156
  for year, group in tqdm(
155
157
  year_groups.items(),
@@ -159,4 +161,5 @@ def generate_wordclouds(
159
161
  text = group.plaintext("user", "assistant")
160
162
  if text.strip():
161
163
  img = generate_wordcloud(text, config)
164
+ # Format: 2024.png
162
165
  img.save(output_dir / f"{year.strftime('%Y')}.png", optimize=True)
convoviz/config.py CHANGED
@@ -10,8 +10,8 @@ from pydantic import BaseModel, Field
10
10
  class FolderOrganization(str, Enum):
11
11
  """How to organize markdown output files in folders."""
12
12
 
13
- FLAT = "flat" # All files in one directory (default)
14
- DATE = "date" # Nested by year/month/week
13
+ FLAT = "flat" # All files in one directory
14
+ DATE = "date" # Nested by year/month (default)
15
15
 
16
16
 
17
17
  class AuthorHeaders(BaseModel):
@@ -27,7 +27,7 @@ class MarkdownConfig(BaseModel):
27
27
  """Configuration for markdown output."""
28
28
 
29
29
  latex_delimiters: Literal["default", "dollars"] = "default"
30
- flavor: Literal["obsidian", "standard"] = "standard"
30
+ flavor: Literal["standard", "obsidian"] = "standard"
31
31
 
32
32
 
33
33
  class YAMLConfig(BaseModel):
convoviz/interactive.py CHANGED
@@ -143,11 +143,11 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
143
143
 
144
144
  # Prompt for markdown flavor
145
145
  flavor_result = cast(
146
- Literal["obsidian", "standard"],
146
+ Literal["standard", "obsidian"],
147
147
  _ask_or_cancel(
148
148
  select(
149
149
  "Select the markdown flavor:",
150
- choices=["obsidian", "standard"],
150
+ choices=["standard", "obsidian"],
151
151
  default=config.conversation.markdown.flavor,
152
152
  style=CUSTOM_STYLE,
153
153
  )
convoviz/io/writers.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from os import utime as os_utime
4
4
  from pathlib import Path
5
+ from urllib.parse import quote
5
6
 
6
7
  from orjson import OPT_INDENT_2, dumps
7
8
  from tqdm import tqdm
@@ -32,8 +33,8 @@ _MONTH_NAMES = [
32
33
  def get_date_folder_path(conversation: Conversation) -> Path:
33
34
  """Get the date-based folder path for a conversation.
34
35
 
35
- Creates a nested structure: year/month/week
36
- Example: 2024/03-March/Week-02/
36
+ Creates a nested structure: year/month
37
+ Example: 2024/03-March/
37
38
 
38
39
  Args:
39
40
  conversation: The conversation to get the path for
@@ -51,13 +52,7 @@ def get_date_folder_path(conversation: Conversation) -> Path:
51
52
  month_name = _MONTH_NAMES[month_num - 1]
52
53
  month = f"{month_num:02d}-{month_name}"
53
54
 
54
- # Week folder: "Week-01" through "Week-05" (week of the month)
55
- # Calculate which week of the month this date falls into
56
- day = create_time.day
57
- week_of_month = (day - 1) // 7 + 1
58
- week = f"Week-{week_of_month:02d}"
59
-
60
- return Path(year) / month / week
55
+ return Path(year) / month
61
56
 
62
57
 
63
58
  def save_conversation(
@@ -115,6 +110,62 @@ def save_conversation(
115
110
  return final_path
116
111
 
117
112
 
113
+ def _generate_year_index(year_dir: Path, year: str) -> None:
114
+ """Generate a _index.md file for a year folder.
115
+
116
+ Args:
117
+ year_dir: Path to the year directory
118
+ year: The year string (e.g., "2024")
119
+ """
120
+ months = sorted(
121
+ [d.name for d in year_dir.iterdir() if d.is_dir()],
122
+ key=lambda m: int(m.split("-")[0]),
123
+ )
124
+
125
+ lines = [
126
+ f"# {year}",
127
+ "",
128
+ "## Months",
129
+ "",
130
+ ]
131
+
132
+ for month in months:
133
+ month_name = month.split("-", 1)[1] if "-" in month else month
134
+ lines.append(f"- [{month_name}]({month}/_index.md)")
135
+
136
+ index_path = year_dir / "_index.md"
137
+ index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
138
+
139
+
140
+ def _generate_month_index(month_dir: Path, year: str, month: str) -> None:
141
+ """Generate a _index.md file for a month folder.
142
+
143
+ Args:
144
+ month_dir: Path to the month directory
145
+ year: The year string (e.g., "2024")
146
+ month: The month folder name (e.g., "03-March")
147
+ """
148
+ month_name = month.split("-", 1)[1] if "-" in month else month
149
+ files = sorted(
150
+ [f.name for f in month_dir.glob("*.md") if f.name != "_index.md"]
151
+ )
152
+
153
+ lines = [
154
+ f"# {month_name} {year}",
155
+ "",
156
+ "## Conversations",
157
+ "",
158
+ ]
159
+
160
+ for file in files:
161
+ title = file[:-3] # Remove .md extension
162
+ encoded_file = quote(file)
163
+ lines.append(f"- [{title}]({encoded_file})")
164
+
165
+ index_path = month_dir / "_index.md"
166
+ index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
167
+
168
+
118
169
  def save_collection(
119
170
  collection: ConversationCollection,
120
171
  directory: Path,
@@ -151,6 +202,15 @@ def save_collection(
151
202
  filepath = target_dir / f"{sanitize(conv.title)}.md"
152
203
  save_conversation(conv, filepath, config, headers, source_path=collection.source_path)
153
204
 
205
+ # Generate index files for date organization
206
+ if folder_organization == FolderOrganization.DATE:
207
+ for year_dir in directory.iterdir():
208
+ if year_dir.is_dir() and year_dir.name.isdigit():
209
+ for month_dir in year_dir.iterdir():
210
+ if month_dir.is_dir():
211
+ _generate_month_index(month_dir, year_dir.name, month_dir.name)
212
+ _generate_year_index(year_dir, year_dir.name)
213
+
154
214
 
155
215
  def save_custom_instructions(
156
216
  collection: ConversationCollection,
@@ -8,24 +8,6 @@ from convoviz.exceptions import MessageContentError
8
8
  from convoviz.models import Conversation, Node
9
9
  from convoviz.renderers.yaml import render_yaml_header
10
10
 
11
- # Length for shortened node IDs in markdown output (similar to Git short hashes)
12
- SHORT_ID_LENGTH = 8
13
-
14
-
15
- def shorten_id(node_id: str) -> str:
16
- """Shorten a node ID for display in markdown.
17
-
18
- Takes the first 8 characters of the ID, which is typically the first
19
- segment of a UUID and provides sufficient uniqueness within a conversation.
20
-
21
- Args:
22
- node_id: The full node ID (often a UUID)
23
-
24
- Returns:
25
- Shortened ID string
26
- """
27
- return node_id[:SHORT_ID_LENGTH]
28
-
29
11
 
30
12
  def close_code_blocks(text: str) -> str:
31
13
  """Ensure all code blocks in the text are properly closed.
@@ -99,15 +81,12 @@ def render_message_header(role: str, headers: AuthorHeaders) -> str:
99
81
  return header_map.get(role, f"### {role.title()}")
100
82
 
101
83
 
102
- def render_node_header(node: Node, headers: AuthorHeaders, flavor: str = "standard") -> str:
84
+ def render_node_header(node: Node, headers: AuthorHeaders) -> str:
103
85
  """Render the header section of a node.
104
86
 
105
- Includes the node ID, parent link, and message author header.
106
-
107
87
  Args:
108
88
  node: The node to render
109
89
  headers: Configuration for author headers
110
- flavor: Markdown flavor (obsidian, standard)
111
90
 
112
91
  Returns:
113
92
  The header markdown string
@@ -115,42 +94,7 @@ def render_node_header(node: Node, headers: AuthorHeaders, flavor: str = "standa
115
94
  if node.message is None:
116
95
  return ""
117
96
 
118
- if flavor == "standard":
119
- return render_message_header(node.message.author.role, headers) + "\n"
120
-
121
- # Obsidian flavor
122
- parts = []
123
-
124
- # Add parent link if parent has a message
125
- if node.parent_node and node.parent_node.message:
126
- parts.append(f"[⬆️](#^{shorten_id(node.parent_node.id)})")
127
-
128
- author_header = render_message_header(node.message.author.role, headers)
129
- parts.append(f"{author_header} ^{shorten_id(node.id)}")
130
-
131
- return "\n".join(parts) + "\n"
132
-
133
-
134
- def render_node_footer(node: Node, flavor: str = "standard") -> str:
135
- """Render the footer section of a node with child links.
136
-
137
- Args:
138
- node: The node to render
139
- flavor: Markdown flavor (obsidian, standard)
140
-
141
- Returns:
142
- The footer markdown string with child navigation links
143
- """
144
- if flavor == "standard" or not node.children_nodes:
145
- return ""
146
-
147
- if len(node.children_nodes) == 1:
148
- return f"\n[⬇️](#^{shorten_id(node.children_nodes[0].id)})\n"
149
-
150
- links = " | ".join(
151
- f"[{i + 1} ⬇️](#^{shorten_id(child.id)})" for i, child in enumerate(node.children_nodes)
152
- )
153
- return f"\n{links}\n"
97
+ return render_message_header(node.message.author.role, headers) + "\n"
154
98
 
155
99
 
156
100
  def render_node(
@@ -158,7 +102,6 @@ def render_node(
158
102
  headers: AuthorHeaders,
159
103
  use_dollar_latex: bool = False,
160
104
  asset_resolver: Callable[[str], str | None] | None = None,
161
- flavor: str = "standard",
162
105
  ) -> str:
163
106
  """Render a complete node as markdown.
164
107
 
@@ -167,7 +110,6 @@ def render_node(
167
110
  headers: Configuration for author headers
168
111
  use_dollar_latex: Whether to convert LaTeX delimiters to dollars
169
112
  asset_resolver: Function to resolve asset IDs to paths
170
- flavor: Markdown flavor (obsidian, standard)
171
113
 
172
114
  Returns:
173
115
  Complete markdown string for the node
@@ -178,7 +120,7 @@ def render_node(
178
120
  if node.message.is_hidden:
179
121
  return ""
180
122
 
181
- header = render_node_header(node, headers, flavor=flavor)
123
+ header = render_node_header(node, headers)
182
124
 
183
125
  # Get and process content
184
126
  try:
@@ -201,9 +143,7 @@ def render_node(
201
143
  # Obsidian handles this well.
202
144
  content += f"\n![Image]({rel_path})\n"
203
145
 
204
- footer = render_node_footer(node, flavor=flavor)
205
-
206
- return f"\n{header}{content}{footer}\n---\n"
146
+ return f"\n{header}{content}\n---\n"
207
147
 
208
148
 
209
149
  def _ordered_nodes(conversation: Conversation) -> list[Node]:
@@ -254,7 +194,7 @@ def render_conversation(
254
194
  Complete markdown document string
255
195
  """
256
196
  use_dollar_latex = config.markdown.latex_delimiters == "dollars"
257
- flavor = config.markdown.flavor
197
+ # Note: config.markdown.flavor is available for future obsidian-specific features
258
198
 
259
199
  # Start with YAML header
260
200
  markdown = render_yaml_header(conversation, config.yaml)
@@ -263,7 +203,7 @@ def render_conversation(
263
203
  for node in _ordered_nodes(conversation):
264
204
  if node.message:
265
205
  markdown += render_node(
266
- node, headers, use_dollar_latex, asset_resolver=asset_resolver, flavor=flavor
206
+ node, headers, use_dollar_latex, asset_resolver=asset_resolver
267
207
  )
268
208
 
269
209
  return markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: convoviz
3
- Version: 0.2.8
3
+ Version: 0.2.10
4
4
  Summary: Get analytics and visualizations on your ChatGPT data!
5
5
  Keywords: markdown,chatgpt,openai,visualization,analytics,json,export,data-analysis,obsidian
6
6
  Author: Mohamed Cheikh Sidiya
@@ -36,7 +36,6 @@ Convert your ChatGPT history into well-formatted Markdown files. Additionally, v
36
36
  - **YAML Headers**: Optional and included by default.
37
37
  - **Inline Images**: Media attachments rendered directly in Markdown.
38
38
  - **Data Visualizations**: Word clouds, graphs, and more.
39
- - **Custom Instructions**: All your custom instructions in one JSON file.
40
39
 
41
40
  See examples [here](demo).
42
41
 
@@ -2,7 +2,7 @@ convoviz/__init__.py,sha256=bQLCHO2U9EyMTGqNgsYiCtBQKTKNj4iIM3-TwIkrnRY,612
2
2
  convoviz/__main__.py,sha256=1qiGW13_SgL7wJi8iioIN-AAHGkNGnEl5q_RcPUrI0s,143
3
3
  convoviz/analysis/__init__.py,sha256=FxgH5JJpyypiLJpMQn_HlM51jnb8lQdP63_C_W3Dlx4,241
4
4
  convoviz/analysis/graphs.py,sha256=gt056UkgGcy9vCkupQmW_HjOLy-W6j4Ekxr315BXPgA,29457
5
- convoviz/analysis/wordcloud.py,sha256=ZnbA_-rcXHwXIny_xbudfJDQbIuPT7urNFfHcx6QWxQ,4673
5
+ convoviz/analysis/wordcloud.py,sha256=WLZhBo5Ha83o6XJyzjhgx4tDjgPX-Q9kizCxfmxlJ3A,4828
6
6
  convoviz/assets/colormaps.txt,sha256=59TSGz428AxY14AEvymAH2IJ2RT9Mlp7Sy0N12NEdXQ,108
7
7
  convoviz/assets/fonts/AmaticSC-Regular.ttf,sha256=83clh7a3urnTLud0_yZofuIb6BdyC2LMI9jhE6G2LvU,142696
8
8
  convoviz/assets/fonts/ArchitectsDaughter-Regular.ttf,sha256=fnrj5_N_SlY2Lj3Ehqz5aKECPZVJlJAflgsOU94_qIM,37756
@@ -37,13 +37,13 @@ convoviz/assets/fonts/YsabeauSC-Thin.ttf,sha256=hZGOZNTRrxbiUPE2VDeLbtnaRwkMOBaV
37
37
  convoviz/assets/fonts/Zeyada-Regular.ttf,sha256=fKhkrp9VHt_3Aw8JfkfkPeC2j3CilLWuPUudzBeawPQ,57468
38
38
  convoviz/assets/stopwords.txt,sha256=7_ywpxsKYOj3U5CZTh9lP4GqbbkZLMabSOjKAXFk6Wc,539
39
39
  convoviz/cli.py,sha256=TPboT0maH_b_EjiT9cWbUSyMFz4ozoqf1_R-4AzY31g,3730
40
- convoviz/config.py,sha256=5fklWzwr0aNyeEJG0NAggLhT0xI0kwgxhjyh9_zUvwM,3112
40
+ convoviz/config.py,sha256=LNVVK6ccTjS4Ip0JczoPIo2fFP1GdqCJOivtzOyZBqs,3107
41
41
  convoviz/exceptions.py,sha256=bQpIKls48uOQpagEJAxpXf5LF7QoagRRfbD0MjWC7Ak,1476
42
- convoviz/interactive.py,sha256=VXtKgYo9tZGtsoj7zThdnbTrbjSNP5MzAZbdOs3icW4,7424
42
+ convoviz/interactive.py,sha256=CwZOfu8a3xat4wGwD7MdVrvCd0rvvbtuufj2yWcGOzg,7424
43
43
  convoviz/io/__init__.py,sha256=y70TYypJ36_kaEA04E2wa1EDaKQVjprKItoKR6MMs4M,471
44
44
  convoviz/io/assets.py,sha256=WLauNEvk9QRo0Q52KE_bPyCRFa1CjM54L1j8SsTfGwg,2894
45
45
  convoviz/io/loaders.py,sha256=RuGiGzpyNcgwTxOM-m2ehhyh2mP1-k1YamK8-VynR3g,5713
46
- convoviz/io/writers.py,sha256=UW-JF5uq91NV62qpFVqgWTYSzzOAkLv67zDpulz2iBc,5072
46
+ convoviz/io/writers.py,sha256=krGvU6UJMrDxZCqt3u_lyLA_gS0oSbeUXhYNFbY4mHo,6862
47
47
  convoviz/models/__init__.py,sha256=6gAfrk6KJT2QxdvX_v15mUdfIqEw1xKxwQlKSfyA5eI,532
48
48
  convoviz/models/collection.py,sha256=L658yKMNC6IZrfxYxZBe-oO9COP_bzVfRznnNon7tfU,4467
49
49
  convoviz/models/conversation.py,sha256=ssx1Z6LM9kJIx3OucQW8JVoAc8zCdxj1iOLtie2B3ak,5678
@@ -52,10 +52,10 @@ convoviz/models/node.py,sha256=1vBAtKVscYsUBDnKAOyLxuZaK9KoVF1dFXiKXRHxUnY,1946
52
52
  convoviz/pipeline.py,sha256=IKfyy3iaNDTqox2YvwB3tnPqvL5iM0i_kMoa854glvY,5806
53
53
  convoviz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  convoviz/renderers/__init__.py,sha256=IQgwD9NqtUgbS9zwyPBNZbBIZcFrbZ9C7WMAV-X3Xdg,261
55
- convoviz/renderers/markdown.py,sha256=mpDt-xrjsPX_wt9URCDk2wicesaVv_VTWWxTHCMKiLM,7765
55
+ convoviz/renderers/markdown.py,sha256=GYsKjwfzLWNWQbJHXEiqWYrEYkWNIdVVZJTIrwSioz0,5926
56
56
  convoviz/renderers/yaml.py,sha256=XG1s4VhDdx-TiqekTkgED87RZ1lVQ7IwrbA-sZHrs7k,4056
57
57
  convoviz/utils.py,sha256=IQEKYHhWOnYxlr4GwAHoquG0BXTlVRkORL80oUSaIeQ,3417
58
- convoviz-0.2.8.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
59
- convoviz-0.2.8.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
60
- convoviz-0.2.8.dist-info/METADATA,sha256=DeO9FBLgHoXlvRDnMiBBSM6qT-h-1bqyQSHqIKb1zrg,5757
61
- convoviz-0.2.8.dist-info/RECORD,,
58
+ convoviz-0.2.10.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
59
+ convoviz-0.2.10.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
60
+ convoviz-0.2.10.dist-info/METADATA,sha256=WtA3BxEud6YZ6HsdYz1xWsSTM_fa_J_A0yf9jsO1DKs,5684
61
+ convoviz-0.2.10.dist-info/RECORD,,