convoviz 0.2.5__py3-none-any.whl → 0.2.7__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.
convoviz/cli.py CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  import typer
6
6
  from rich.console import Console
7
7
 
8
- from convoviz.config import get_default_config
8
+ from convoviz.config import FolderOrganization, get_default_config
9
9
  from convoviz.exceptions import ConfigurationError, InvalidZipError
10
10
  from convoviz.interactive import run_interactive_config
11
11
  from convoviz.io.loaders import find_latest_zip
@@ -38,6 +38,12 @@ def run(
38
38
  "-o",
39
39
  help="Path to the output directory.",
40
40
  ),
41
+ flat: bool = typer.Option(
42
+ False,
43
+ "--flat",
44
+ "-f",
45
+ help="Put all markdown files in a single folder (disables date organization).",
46
+ ),
41
47
  interactive: bool | None = typer.Option(
42
48
  None,
43
49
  "--interactive/--no-interactive",
@@ -57,6 +63,8 @@ def run(
57
63
  config.input_path = input_path
58
64
  if output_dir:
59
65
  config.output_folder = output_dir
66
+ if flat:
67
+ config.folder_organization = FolderOrganization.FLAT
60
68
 
61
69
  # Determine mode: interactive if explicitly requested or no input provided
62
70
  use_interactive = interactive if interactive is not None else (input_path is None)
convoviz/config.py CHANGED
@@ -1,11 +1,19 @@
1
1
  """Configuration models using Pydantic v2."""
2
2
 
3
+ from enum import Enum
3
4
  from pathlib import Path
4
5
  from typing import Literal
5
6
 
6
7
  from pydantic import BaseModel, Field
7
8
 
8
9
 
10
+ class FolderOrganization(str, Enum):
11
+ """How to organize markdown output files in folders."""
12
+
13
+ FLAT = "flat" # All files in one directory (default)
14
+ DATE = "date" # Nested by year/month/week
15
+
16
+
9
17
  class AuthorHeaders(BaseModel):
10
18
  """Headers for different message authors in markdown output."""
11
19
 
@@ -74,6 +82,8 @@ class GraphConfig(BaseModel):
74
82
  figsize: tuple[int, int] = (10, 6)
75
83
  dpi: int = 300
76
84
  timezone: Literal["utc", "local"] = "local"
85
+ generate_monthly_breakdowns: bool = False
86
+ generate_yearly_breakdowns: bool = False
77
87
 
78
88
 
79
89
  class ConvovizConfig(BaseModel):
@@ -81,6 +91,7 @@ class ConvovizConfig(BaseModel):
81
91
 
82
92
  input_path: Path | None = None
83
93
  output_folder: Path = Field(default_factory=lambda: Path.home() / "Documents" / "ChatGPT-Data")
94
+ folder_organization: FolderOrganization = FolderOrganization.DATE
84
95
  message: MessageConfig = Field(default_factory=MessageConfig)
85
96
  conversation: ConversationConfig = Field(default_factory=ConversationConfig)
86
97
  wordcloud: WordCloudConfig = Field(default_factory=WordCloudConfig)
convoviz/io/writers.py CHANGED
@@ -6,12 +6,59 @@ from pathlib import Path
6
6
  from orjson import OPT_INDENT_2, dumps
7
7
  from tqdm import tqdm
8
8
 
9
- from convoviz.config import AuthorHeaders, ConversationConfig
9
+ from convoviz.config import AuthorHeaders, ConversationConfig, FolderOrganization
10
10
  from convoviz.io.assets import copy_asset, resolve_asset_path
11
11
  from convoviz.models import Conversation, ConversationCollection
12
12
  from convoviz.renderers import render_conversation
13
13
  from convoviz.utils import sanitize
14
14
 
15
+ # Month names for folder naming
16
+ _MONTH_NAMES = [
17
+ "January",
18
+ "February",
19
+ "March",
20
+ "April",
21
+ "May",
22
+ "June",
23
+ "July",
24
+ "August",
25
+ "September",
26
+ "October",
27
+ "November",
28
+ "December",
29
+ ]
30
+
31
+
32
+ def get_date_folder_path(conversation: Conversation) -> Path:
33
+ """Get the date-based folder path for a conversation.
34
+
35
+ Creates a nested structure: year/month/week
36
+ Example: 2024/03-March/Week-02/
37
+
38
+ Args:
39
+ conversation: The conversation to get the path for
40
+
41
+ Returns:
42
+ Relative path for the date-based folder structure
43
+ """
44
+ create_time = conversation.create_time
45
+
46
+ # Year folder: "2024"
47
+ year = str(create_time.year)
48
+
49
+ # Month folder: "03-March"
50
+ month_num = create_time.month
51
+ month_name = _MONTH_NAMES[month_num - 1]
52
+ month = f"{month_num:02d}-{month_name}"
53
+
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
61
+
15
62
 
16
63
  def save_conversation(
17
64
  conversation: Conversation,
@@ -74,6 +121,7 @@ def save_collection(
74
121
  config: ConversationConfig,
75
122
  headers: AuthorHeaders,
76
123
  *,
124
+ folder_organization: FolderOrganization = FolderOrganization.FLAT,
77
125
  progress_bar: bool = False,
78
126
  ) -> None:
79
127
  """Save all conversations in a collection to markdown files.
@@ -83,6 +131,7 @@ def save_collection(
83
131
  directory: Target directory
84
132
  config: Conversation rendering configuration
85
133
  headers: Author header configuration
134
+ folder_organization: How to organize files in folders (flat or by date)
86
135
  progress_bar: Whether to show a progress bar
87
136
  """
88
137
  directory.mkdir(parents=True, exist_ok=True)
@@ -92,7 +141,14 @@ def save_collection(
92
141
  desc="Writing Markdown 📄 files",
93
142
  disable=not progress_bar,
94
143
  ):
95
- filepath = directory / f"{sanitize(conv.title)}.md"
144
+ # Determine target directory based on organization setting
145
+ if folder_organization == FolderOrganization.DATE:
146
+ target_dir = directory / get_date_folder_path(conv)
147
+ target_dir.mkdir(parents=True, exist_ok=True)
148
+ else:
149
+ target_dir = directory
150
+
151
+ filepath = target_dir / f"{sanitize(conv.title)}.md"
96
152
  save_conversation(conv, filepath, config, headers, source_path=collection.source_path)
97
153
 
98
154
 
convoviz/pipeline.py CHANGED
@@ -113,6 +113,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
113
113
  markdown_folder,
114
114
  config.conversation,
115
115
  config.message.author_headers,
116
+ folder_organization=config.folder_organization,
116
117
  progress_bar=True,
117
118
  )
118
119
  console.print(
@@ -8,6 +8,24 @@ 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
+
11
29
 
12
30
  def close_code_blocks(text: str) -> str:
13
31
  """Ensure all code blocks in the text are properly closed.
@@ -105,10 +123,10 @@ def render_node_header(node: Node, headers: AuthorHeaders, flavor: str = "standa
105
123
 
106
124
  # Add parent link if parent has a message
107
125
  if node.parent_node and node.parent_node.message:
108
- parts.append(f"[⬆️](#^{node.parent_node.id})")
126
+ parts.append(f"[⬆️](#^{shorten_id(node.parent_node.id)})")
109
127
 
110
128
  author_header = render_message_header(node.message.author.role, headers)
111
- parts.append(f"{author_header} ^{node.id}")
129
+ parts.append(f"{author_header} ^{shorten_id(node.id)}")
112
130
 
113
131
  return "\n".join(parts) + "\n"
114
132
 
@@ -127,9 +145,11 @@ def render_node_footer(node: Node, flavor: str = "standard") -> str:
127
145
  return ""
128
146
 
129
147
  if len(node.children_nodes) == 1:
130
- return f"\n[⬇️](#^{node.children_nodes[0].id})\n"
148
+ return f"\n[⬇️](#^{shorten_id(node.children_nodes[0].id)})\n"
131
149
 
132
- links = " | ".join(f"[{i + 1} ⬇️](#^{child.id})" for i, child in enumerate(node.children_nodes))
150
+ links = " | ".join(
151
+ f"[{i + 1} ⬇️](#^{shorten_id(child.id)})" for i, child in enumerate(node.children_nodes)
152
+ )
133
153
  return f"\n{links}\n"
134
154
 
135
155
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: convoviz
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -84,11 +84,6 @@ You can provide arguments directly to skip the prompts:
84
84
  convoviz --input path/to/your/export.zip --output path/to/output/folder
85
85
  ```
86
86
 
87
- Inputs can be any of:
88
- - A ChatGPT export ZIP (downloaded from OpenAI)
89
- - An extracted export directory containing `conversations.json`
90
- - A `conversations.json` file directly
91
-
92
87
  Notes:
93
88
  - `--zip` / `-z` is kept as an alias for `--input` for convenience.
94
89
  - You can force non-interactive mode with `--no-interactive`.
@@ -101,7 +96,19 @@ convoviz --help
101
96
 
102
97
  ### 4. Check the Output 🎉
103
98
 
104
- And that's it! After running the script, head over to the output folder to see your nice word clouds, graphs, and neatly formatted Markdown files. Enjoy !
99
+ And that's it! After running the script, head over to the output folder to see your neatly formatted Markdown files and visualizations.
100
+
101
+ The main outputs are:
102
+
103
+ - **`Markdown/`**: one `.md` file per conversation
104
+ - **`Graphs/`**: a small set of high-signal plots, including:
105
+ - `overview.png` (dashboard)
106
+ - `activity_heatmap.png` (weekday × hour)
107
+ - `daily_activity.png` / `monthly_activity.png`
108
+ - `model_usage.png`, `conversation_lengths.png`
109
+ - `weekday_pattern.png`, `hourly_pattern.png`, `conversation_lifetimes.png`
110
+ - **`Word-Clouds/`**: weekly/monthly/yearly word clouds
111
+ - **`custom_instructions.json`**: extracted custom instructions
105
112
 
106
113
  ## Share Your Feedback! 💌
107
114
 
@@ -119,7 +126,7 @@ And if you've had a great experience, consider giving the project a star ⭐. It
119
126
 
120
127
  ## Notes
121
128
 
122
- This is just a small thing I coded to help me see my convos in beautiful markdown, in [Obsidian](https://obsidian.md/) (my go-to note-taking app).
129
+ This is just a small thing I coded to help me see my convos in beautiful markdown. It was originally built with [Obsidian](https://obsidian.md/) (my go-to note-taking app) in mind, but the default output is standard Markdown (and you can choose an Obsidian-flavored mode in the interactive config if you want block IDs / navigation links).
123
130
 
124
131
  I wasn't a fan of the clunky, and sometimes paid, browser extensions.
125
132
 
@@ -1,7 +1,7 @@
1
1
  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
- convoviz/analysis/graphs.py,sha256=3CV4yhFwfUYb5-CXtq4D-r_vf0jn5cxDXwaPu1P8M8g,14928
4
+ convoviz/analysis/graphs.py,sha256=gt056UkgGcy9vCkupQmW_HjOLy-W6j4Ekxr315BXPgA,29457
5
5
  convoviz/analysis/wordcloud.py,sha256=ZnbA_-rcXHwXIny_xbudfJDQbIuPT7urNFfHcx6QWxQ,4673
6
6
  convoviz/assets/colormaps.txt,sha256=59TSGz428AxY14AEvymAH2IJ2RT9Mlp7Sy0N12NEdXQ,108
7
7
  convoviz/assets/fonts/AmaticSC-Regular.ttf,sha256=83clh7a3urnTLud0_yZofuIb6BdyC2LMI9jhE6G2LvU,142696
@@ -36,26 +36,26 @@ convoviz/assets/fonts/YsabeauSC-Regular.ttf,sha256=G4lkq34KKqZOaoomtxFz_KlwVmxg5
36
36
  convoviz/assets/fonts/YsabeauSC-Thin.ttf,sha256=hZGOZNTRrxbiUPE2VDeLbtnaRwkMOBaVQbq7Fwx-34c,116932
37
37
  convoviz/assets/fonts/Zeyada-Regular.ttf,sha256=fKhkrp9VHt_3Aw8JfkfkPeC2j3CilLWuPUudzBeawPQ,57468
38
38
  convoviz/assets/stopwords.txt,sha256=7_ywpxsKYOj3U5CZTh9lP4GqbbkZLMabSOjKAXFk6Wc,539
39
- convoviz/cli.py,sha256=8HNn-6kmDN8ECb0BspvjeGa_636SQPDffpM0yINgNII,3463
40
- convoviz/config.py,sha256=vjedCcpQ_t-mR6cZ4GJJuyRPDeY95XCIiMXufVIlm9M,2724
39
+ convoviz/cli.py,sha256=TPboT0maH_b_EjiT9cWbUSyMFz4ozoqf1_R-4AzY31g,3730
40
+ convoviz/config.py,sha256=5fklWzwr0aNyeEJG0NAggLhT0xI0kwgxhjyh9_zUvwM,3112
41
41
  convoviz/exceptions.py,sha256=bQpIKls48uOQpagEJAxpXf5LF7QoagRRfbD0MjWC7Ak,1476
42
42
  convoviz/interactive.py,sha256=VXtKgYo9tZGtsoj7zThdnbTrbjSNP5MzAZbdOs3icW4,7424
43
43
  convoviz/io/__init__.py,sha256=y70TYypJ36_kaEA04E2wa1EDaKQVjprKItoKR6MMs4M,471
44
44
  convoviz/io/assets.py,sha256=BykidWJG6OQAgbVfUByQ3RLTrldzpZ_NeM7HV3a5Tig,2333
45
45
  convoviz/io/loaders.py,sha256=RuGiGzpyNcgwTxOM-m2ehhyh2mP1-k1YamK8-VynR3g,5713
46
- convoviz/io/writers.py,sha256=KaLr0f2F2Pw5XOoQKMA75IeQYXUTT4WbS-HAqRxsp3c,3494
46
+ convoviz/io/writers.py,sha256=UW-JF5uq91NV62qpFVqgWTYSzzOAkLv67zDpulz2iBc,5072
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
50
50
  convoviz/models/message.py,sha256=mVnaUG6hypz92Oz3OgFAK1uuTgH3ZOJAWsFiCpLYneY,5459
51
51
  convoviz/models/node.py,sha256=1vBAtKVscYsUBDnKAOyLxuZaK9KoVF1dFXiKXRHxUnY,1946
52
- convoviz/pipeline.py,sha256=Mwg3Xqazk5PrsIHxhVajtWbfq4PgFlIGVHWq8BsW0U0,5750
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=HDvTYpTJUI87o8QjS5ZfMS1FLRS4zPNBvCDyWzEpi9o,7211
55
+ convoviz/renderers/markdown.py,sha256=mpDt-xrjsPX_wt9URCDk2wicesaVv_VTWWxTHCMKiLM,7765
56
56
  convoviz/renderers/yaml.py,sha256=XG1s4VhDdx-TiqekTkgED87RZ1lVQ7IwrbA-sZHrs7k,4056
57
57
  convoviz/utils.py,sha256=IQEKYHhWOnYxlr4GwAHoquG0BXTlVRkORL80oUSaIeQ,3417
58
- convoviz-0.2.5.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
59
- convoviz-0.2.5.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
60
- convoviz-0.2.5.dist-info/METADATA,sha256=nh8J1XdXD9CdGO3REyBLZTdan-LdCP92tofpY7w4Wt0,5309
61
- convoviz-0.2.5.dist-info/RECORD,,
58
+ convoviz-0.2.7.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
59
+ convoviz-0.2.7.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
60
+ convoviz-0.2.7.dist-info/METADATA,sha256=B3v-e0XrLNCEos-nS6_Ynw3-KGPxDMpvbrFgSEFYyHw,5820
61
+ convoviz-0.2.7.dist-info/RECORD,,