convoviz 0.3.2__tar.gz → 0.3.9__tar.gz

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.
Files changed (61) hide show
  1. {convoviz-0.3.2 → convoviz-0.3.9}/PKG-INFO +12 -5
  2. {convoviz-0.3.2 → convoviz-0.3.9}/README.md +11 -4
  3. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/analysis/graphs.py +4 -0
  4. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/analysis/wordcloud.py +8 -0
  5. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/cli.py +26 -2
  6. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/interactive.py +9 -0
  7. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/io/assets.py +12 -1
  8. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/io/loaders.py +5 -0
  9. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/io/writers.py +6 -0
  10. convoviz-0.3.9/convoviz/logging_config.py +77 -0
  11. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/pipeline.py +8 -0
  12. {convoviz-0.3.2 → convoviz-0.3.9}/pyproject.toml +1 -1
  13. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/__init__.py +0 -0
  14. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/__main__.py +0 -0
  15. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/analysis/__init__.py +0 -0
  16. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/colormaps.txt +0 -0
  17. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/AmaticSC-Regular.ttf +0 -0
  18. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/ArchitectsDaughter-Regular.ttf +0 -0
  19. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/BebasNeue-Regular.ttf +0 -0
  20. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Borel-Regular.ttf +0 -0
  21. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Courgette-Regular.ttf +0 -0
  22. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/CroissantOne-Regular.ttf +0 -0
  23. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Handjet-Regular.ttf +0 -0
  24. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/IndieFlower-Regular.ttf +0 -0
  25. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Kalam-Regular.ttf +0 -0
  26. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Lobster-Regular.ttf +0 -0
  27. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/MartianMono-Regular.ttf +0 -0
  28. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/MartianMono-Thin.ttf +0 -0
  29. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Montserrat-Regular.ttf +0 -0
  30. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Mooli-Regular.ttf +0 -0
  31. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Pacifico-Regular.ttf +0 -0
  32. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/PlayfairDisplay-Regular.ttf +0 -0
  33. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Raleway-Regular.ttf +0 -0
  34. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/RobotoMono-Regular.ttf +0 -0
  35. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/RobotoMono-Thin.ttf +0 -0
  36. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/RobotoSlab-Regular.ttf +0 -0
  37. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/RobotoSlab-Thin.ttf +0 -0
  38. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Ruwudu-Regular.ttf +0 -0
  39. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Sacramento-Regular.ttf +0 -0
  40. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/SedgwickAveDisplay-Regular.ttf +0 -0
  41. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/ShadowsIntoLight-Regular.ttf +0 -0
  42. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/TitilliumWeb-Regular.ttf +0 -0
  43. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Yellowtail-Regular.ttf +0 -0
  44. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/YsabeauOffice-Regular.ttf +0 -0
  45. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/YsabeauSC-Regular.ttf +0 -0
  46. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/YsabeauSC-Thin.ttf +0 -0
  47. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/fonts/Zeyada-Regular.ttf +0 -0
  48. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/assets/stopwords.txt +0 -0
  49. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/config.py +0 -0
  50. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/exceptions.py +0 -0
  51. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/io/__init__.py +0 -0
  52. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/models/__init__.py +0 -0
  53. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/models/collection.py +0 -0
  54. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/models/conversation.py +0 -0
  55. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/models/message.py +0 -0
  56. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/models/node.py +0 -0
  57. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/py.typed +0 -0
  58. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/renderers/__init__.py +0 -0
  59. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/renderers/markdown.py +0 -0
  60. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/renderers/yaml.py +0 -0
  61. {convoviz-0.3.2 → convoviz-0.3.9}/convoviz/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: convoviz
3
- Version: 0.3.2
3
+ Version: 0.3.9
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
@@ -27,9 +27,6 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  Convert your ChatGPT history into well-formatted Markdown files. Visualize your data with word clouds 🔡☁️ and usage graphs 📈.
29
29
 
30
- ![GitHub last commit](https://img.shields.io/github/last-commit/mohamed-chs/chatgpt-history-export-to-md)
31
- ![GitHub issues](https://img.shields.io/github/issues/mohamed-chs/chatgpt-history-export-to-md)
32
-
33
30
  ## Features
34
31
 
35
32
  - **YAML Headers**: Optional and included by default.
@@ -56,7 +53,7 @@ MacOS / Linux:
56
53
  curl -LsSf https://astral.sh/uv/install.sh | sh
57
54
  ```
58
55
 
59
- Windows (PowerShell):
56
+ Windows:
60
57
 
61
58
  ```powershell
62
59
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
@@ -70,6 +67,14 @@ uv tool install "convoviz[viz]"
70
67
 
71
68
  The `[viz]` extra includes graphs and word clouds. Skip it for a faster markdown-only install.
72
69
 
70
+ ### Alternative: pip
71
+
72
+ ```bash
73
+ python3 -m venv .venv
74
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
75
+ pip install "convoviz[viz]"
76
+ ```
77
+
73
78
  ### 3. Run the tool 🏃‍♂️
74
79
 
75
80
  Simply run the command and follow the prompts:
@@ -101,6 +106,8 @@ Options: `markdown`, `graphs`, `wordclouds`. In interactive mode, you'll be prom
101
106
  - `--zip` / `-z` is kept as an alias for `--input` for convenience.
102
107
  - You can force non-interactive mode with `--no-interactive`.
103
108
  - Use `--flat` to put all Markdown files in a single folder instead of organizing by date.
109
+ - Use `--verbose` or `-v` for detailed logging (use `-vv` for debug logs).
110
+ - Use `--log-file` to specify a custom log file (logs default to a temporary file if not specified).
104
111
 
105
112
  For more options, run:
106
113
 
@@ -2,9 +2,6 @@
2
2
 
3
3
  Convert your ChatGPT history into well-formatted Markdown files. Visualize your data with word clouds 🔡☁️ and usage graphs 📈.
4
4
 
5
- ![GitHub last commit](https://img.shields.io/github/last-commit/mohamed-chs/chatgpt-history-export-to-md)
6
- ![GitHub issues](https://img.shields.io/github/issues/mohamed-chs/chatgpt-history-export-to-md)
7
-
8
5
  ## Features
9
6
 
10
7
  - **YAML Headers**: Optional and included by default.
@@ -31,7 +28,7 @@ MacOS / Linux:
31
28
  curl -LsSf https://astral.sh/uv/install.sh | sh
32
29
  ```
33
30
 
34
- Windows (PowerShell):
31
+ Windows:
35
32
 
36
33
  ```powershell
37
34
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
@@ -45,6 +42,14 @@ uv tool install "convoviz[viz]"
45
42
 
46
43
  The `[viz]` extra includes graphs and word clouds. Skip it for a faster markdown-only install.
47
44
 
45
+ ### Alternative: pip
46
+
47
+ ```bash
48
+ python3 -m venv .venv
49
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
50
+ pip install "convoviz[viz]"
51
+ ```
52
+
48
53
  ### 3. Run the tool 🏃‍♂️
49
54
 
50
55
  Simply run the command and follow the prompts:
@@ -76,6 +81,8 @@ Options: `markdown`, `graphs`, `wordclouds`. In interactive mode, you'll be prom
76
81
  - `--zip` / `-z` is kept as an alias for `--input` for convenience.
77
82
  - You can force non-interactive mode with `--no-interactive`.
78
83
  - Use `--flat` to put all Markdown files in a single folder instead of organizing by date.
84
+ - Use `--verbose` or `-v` for detailed logging (use `-vv` for debug logs).
85
+ - Use `--log-file` to specify a custom log file (logs default to a temporary file if not specified).
79
86
 
80
87
  For more options, run:
81
88
 
@@ -12,6 +12,7 @@ from collections import defaultdict
12
12
  from collections.abc import Callable, Iterable
13
13
  from datetime import UTC, datetime
14
14
  from pathlib import Path
15
+ import logging
15
16
 
16
17
  import matplotlib.dates as mdates
17
18
  import matplotlib.font_manager as fm
@@ -25,6 +26,8 @@ from convoviz.config import GraphConfig, get_default_config
25
26
  from convoviz.models import ConversationCollection
26
27
  from convoviz.utils import get_asset_path
27
28
 
29
+ logger = logging.getLogger(__name__)
30
+
28
31
  WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
29
32
 
30
33
 
@@ -742,6 +745,7 @@ def generate_summary_graphs(
742
745
  cfg = config or get_default_config().graph
743
746
 
744
747
  user_ts = collection.timestamps("user")
748
+ logger.info(f"Generating summary graphs to {output_dir}")
745
749
 
746
750
  tasks: list[tuple[str, str, Callable[[], Figure]]] = [
747
751
  ("Overview", "overview.png", lambda: generate_summary_dashboard(collection, cfg)),
@@ -4,6 +4,7 @@ import os
4
4
  from concurrent.futures import ProcessPoolExecutor
5
5
  from functools import lru_cache
6
6
  from pathlib import Path
7
+ import logging
7
8
 
8
9
  from nltk import download as nltk_download
9
10
  from nltk.corpus import stopwords as nltk_stopwords
@@ -15,6 +16,8 @@ from wordcloud import WordCloud
15
16
  from convoviz.config import WordCloudConfig
16
17
  from convoviz.models import ConversationCollection
17
18
 
19
+ logger = logging.getLogger(__name__)
20
+
18
21
  # Languages for stopwords
19
22
  STOPWORD_LANGUAGES = [
20
23
  "arabic",
@@ -149,11 +152,15 @@ def generate_wordclouds(
149
152
  progress_bar: Whether to show progress bars
150
153
  """
151
154
  output_dir.mkdir(parents=True, exist_ok=True)
155
+ logger.info(f"Generating wordclouds to {output_dir}")
152
156
 
153
157
  week_groups = collection.group_by_week()
154
158
  month_groups = collection.group_by_month()
155
159
  year_groups = collection.group_by_year()
156
160
 
161
+ # Pre-load/download NLTK stopwords in the main process to avoid race conditions in workers
162
+ load_nltk_stopwords()
163
+
157
164
  # Build list of all tasks: (text, filename, output_dir, config)
158
165
  tasks: list[tuple[str, str, Path, WordCloudConfig]] = []
159
166
 
@@ -185,6 +192,7 @@ def generate_wordclouds(
185
192
  max_workers = max(1, cpu_count // 2)
186
193
 
187
194
  # Use parallel processing for speedup on multi-core systems
195
+ logger.debug(f"Starting wordcloud generation with {max_workers} workers for {len(tasks)} tasks")
188
196
  with ProcessPoolExecutor(max_workers=max_workers) as executor:
189
197
  list(
190
198
  tqdm(
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
 
5
5
  import typer
6
6
  from rich.console import Console
7
+ from rich.markup import escape
7
8
 
8
9
  from convoviz.config import FolderOrganization, OutputKind, get_default_config
9
10
  from convoviz.exceptions import ConfigurationError, InvalidZipError
@@ -11,6 +12,8 @@ from convoviz.interactive import run_interactive_config
11
12
  from convoviz.io.loaders import find_latest_zip
12
13
  from convoviz.pipeline import run_pipeline
13
14
  from convoviz.utils import default_font_path
15
+ from convoviz.logging_config import setup_logging
16
+ import logging
14
17
 
15
18
  app = typer.Typer(
16
19
  add_completion=False,
@@ -56,8 +59,26 @@ def run(
56
59
  "-i/-I",
57
60
  help="Force interactive mode on or off.",
58
61
  ),
62
+ verbose: int = typer.Option(
63
+ 0,
64
+ "--verbose",
65
+ "-v",
66
+ help="Increase verbosity. Use -vv for debug.",
67
+ count=True,
68
+ ),
69
+ log_file: Path | None = typer.Option(
70
+ None,
71
+ "--log-file",
72
+ help="Path to log file. Defaults to a temporary file.",
73
+ ),
59
74
  ) -> None:
60
75
  """Convert ChatGPT export data to markdown and generate visualizations."""
76
+ # Setup logging immediately
77
+ log_path = setup_logging(verbose, log_file)
78
+ logger = logging.getLogger("convoviz.cli")
79
+ console.print(f"[dim]Logging to: {log_path}[/dim]")
80
+ logger.debug(f"Logging initialized. Output: {log_path}")
81
+
61
82
  if ctx.invoked_subcommand is not None:
62
83
  return
63
84
 
@@ -113,10 +134,13 @@ def run(
113
134
  try:
114
135
  run_pipeline(config)
115
136
  except (InvalidZipError, ConfigurationError) as e:
116
- console.print(f"[bold red]Error:[/bold red] {e}")
137
+ logger.error(f"Known error: {e}")
138
+ console.print(f"[bold red]Error:[/bold red] {escape(str(e))}")
117
139
  raise typer.Exit(code=1) from None
118
140
  except Exception as e:
119
- console.print(f"[bold red]Unexpected error:[/bold red] {e}")
141
+ logger.exception("Unexpected error occurred")
142
+ console.print(f"[bold red]Unexpected error:[/bold red] {escape(str(e))}")
143
+ console.print(f"[dim]See log file for details: {log_path}[/dim]")
120
144
  raise typer.Exit(code=1) from None
121
145
 
122
146
 
@@ -6,6 +6,7 @@ from typing import Literal, Protocol, cast
6
6
  from questionary import Choice, Style, checkbox, select
7
7
  from questionary import path as qst_path
8
8
  from questionary import text as qst_text
9
+ import logging
9
10
 
10
11
  from convoviz.config import ConvovizConfig, OutputKind, get_default_config
11
12
  from convoviz.io.loaders import find_latest_zip, validate_zip
@@ -26,6 +27,8 @@ CUSTOM_STYLE = Style(
26
27
  ]
27
28
  )
28
29
 
30
+ logger = logging.getLogger(__name__)
31
+
29
32
 
30
33
  class _QuestionaryPrompt[T](Protocol):
31
34
  def ask(self) -> T | None: ...
@@ -74,6 +77,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
74
77
  Updated configuration based on user input
75
78
  """
76
79
  config = initial_config or get_default_config()
80
+ logger.info("Starting interactive configuration")
77
81
 
78
82
  # Set sensible defaults if not already set
79
83
  if not config.input_path:
@@ -97,6 +101,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
97
101
 
98
102
  if input_result:
99
103
  config.input_path = Path(input_result)
104
+ logger.debug(f"User selected input: {config.input_path}")
100
105
 
101
106
  # Prompt for output folder
102
107
  output_result: str = _ask_or_cancel(
@@ -109,6 +114,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
109
114
 
110
115
  if output_result:
111
116
  config.output_folder = Path(output_result)
117
+ logger.debug(f"User selected output: {config.output_folder}")
112
118
 
113
119
  # Prompt for outputs to generate
114
120
  output_choices = [
@@ -126,6 +132,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
126
132
  )
127
133
 
128
134
  config.outputs = set(selected_outputs) if selected_outputs else set()
135
+ logger.debug(f"User selected outputs: {config.outputs}")
129
136
 
130
137
  # Prompt for markdown settings (only if markdown output is selected)
131
138
  if OutputKind.MARKDOWN in config.outputs:
@@ -144,6 +151,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
144
151
  )
145
152
  if result:
146
153
  setattr(headers, role, result)
154
+ logger.debug(f"User selected headers: {headers}")
147
155
 
148
156
  # Prompt for markdown flavor
149
157
  flavor_result = cast(
@@ -160,6 +168,7 @@ def run_interactive_config(initial_config: ConvovizConfig | None = None) -> Conv
160
168
 
161
169
  if flavor_result:
162
170
  config.conversation.markdown.flavor = flavor_result
171
+ logger.debug(f"User selected flavor: {config.conversation.markdown.flavor}")
163
172
 
164
173
  # Prompt for YAML headers
165
174
  yaml_config = config.conversation.yaml
@@ -2,6 +2,9 @@
2
2
 
3
3
  import shutil
4
4
  from pathlib import Path
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
5
8
 
6
9
 
7
10
  def resolve_asset_path(source_dir: Path, asset_id: str) -> Path | None:
@@ -26,6 +29,7 @@ def resolve_asset_path(source_dir: Path, asset_id: str) -> Path | None:
26
29
  # 1. Try exact match
27
30
  exact_path = (source_dir / asset_id).resolve()
28
31
  if exact_path.exists() and exact_path.is_file() and exact_path.is_relative_to(source_dir):
32
+ logger.debug(f"Resolved asset (exact): {asset_id} -> {exact_path}")
29
33
  return exact_path
30
34
 
31
35
  # 2. Try prefix match in root
@@ -37,6 +41,7 @@ def resolve_asset_path(source_dir: Path, asset_id: str) -> Path | None:
37
41
  if p.is_file() and p.resolve().is_relative_to(source_dir)
38
42
  ]
39
43
  if files:
44
+ logger.debug(f"Resolved asset (prefix root): {asset_id} -> {files[0]}")
40
45
  return files[0]
41
46
  except Exception:
42
47
  pass
@@ -53,6 +58,7 @@ def resolve_asset_path(source_dir: Path, asset_id: str) -> Path | None:
53
58
  if p.is_file() and p.resolve().is_relative_to(dalle_dir)
54
59
  ]
55
60
  if files:
61
+ logger.debug(f"Resolved asset (dalle): {asset_id} -> {files[0]}")
56
62
  return files[0]
57
63
  except Exception:
58
64
  pass
@@ -69,6 +75,7 @@ def resolve_asset_path(source_dir: Path, asset_id: str) -> Path | None:
69
75
  if p.is_file() and p.resolve().is_relative_to(user_dir)
70
76
  ]
71
77
  if files:
78
+ logger.debug(f"Resolved asset (user dir): {asset_id} -> {files[0]}")
72
79
  return files[0]
73
80
  except Exception:
74
81
  pass
@@ -92,7 +99,11 @@ def copy_asset(source_path: Path, dest_dir: Path) -> str:
92
99
  dest_path = assets_dir / source_path.name
93
100
 
94
101
  if not dest_path.exists():
95
- shutil.copy2(source_path, dest_path)
102
+ try:
103
+ shutil.copy2(source_path, dest_path)
104
+ logger.debug(f"Copied asset: {source_path.name}")
105
+ except Exception as e:
106
+ logger.warning(f"Failed to copy asset {source_path}: {e}")
96
107
 
97
108
  # Return forward-slash path for Markdown compatibility even on Windows
98
109
  return f"assets/{source_path.name}"
@@ -2,12 +2,15 @@
2
2
 
3
3
  from pathlib import Path, PurePosixPath
4
4
  from zipfile import ZipFile
5
+ import logging
5
6
 
6
7
  from orjson import loads
7
8
 
8
9
  from convoviz.exceptions import InvalidZipError
9
10
  from convoviz.models import Conversation, ConversationCollection
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
11
14
 
12
15
  def _is_safe_zip_member_name(name: str) -> bool:
13
16
  """Return True if a ZIP entry name is safe to extract.
@@ -46,6 +49,7 @@ def extract_archive(filepath: Path) -> Path:
46
49
  """
47
50
  folder = filepath.with_suffix("")
48
51
  folder.mkdir(parents=True, exist_ok=True)
52
+ logger.info(f"Extracting archive: {filepath} to {folder}")
49
53
 
50
54
  with ZipFile(filepath) as zf:
51
55
  for member in zf.infolist():
@@ -115,6 +119,7 @@ def load_collection_from_json(filepath: Path | str) -> ConversationCollection:
115
119
  Loaded ConversationCollection object
116
120
  """
117
121
  filepath = Path(filepath)
122
+ logger.debug(f"Loading collection from JSON: {filepath}")
118
123
  with filepath.open(encoding="utf-8") as f:
119
124
  data = loads(f.read())
120
125
 
@@ -12,6 +12,9 @@ from convoviz.io.assets import copy_asset, resolve_asset_path
12
12
  from convoviz.models import Conversation, ConversationCollection
13
13
  from convoviz.renderers import render_conversation
14
14
  from convoviz.utils import sanitize
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
15
18
 
16
19
  # Month names for folder naming
17
20
  _MONTH_NAMES = [
@@ -102,6 +105,7 @@ def save_conversation(
102
105
  markdown = render_conversation(conversation, config, headers, asset_resolver=asset_resolver)
103
106
  with final_path.open("w", encoding="utf-8") as f:
104
107
  f.write(markdown)
108
+ logger.debug(f"Saved conversation: {final_path}")
105
109
 
106
110
  # Set modification time
107
111
  timestamp = conversation.update_time.timestamp()
@@ -135,6 +139,7 @@ def _generate_year_index(year_dir: Path, year: str) -> None:
135
139
 
136
140
  index_path = year_dir / "_index.md"
137
141
  index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
142
+ logger.debug(f"Generated year index: {index_path}")
138
143
 
139
144
 
140
145
  def _generate_month_index(month_dir: Path, year: str, month: str) -> None:
@@ -162,6 +167,7 @@ def _generate_month_index(month_dir: Path, year: str, month: str) -> None:
162
167
 
163
168
  index_path = month_dir / "_index.md"
164
169
  index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
170
+ logger.debug(f"Generated month index: {index_path}")
165
171
 
166
172
 
167
173
  def save_collection(
@@ -0,0 +1,77 @@
1
+ """Logging configuration for convoviz."""
2
+
3
+ import logging
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from rich.logging import RichHandler
10
+
11
+ def setup_logging(
12
+ verbosity: int = 0,
13
+ log_file: Optional[Path] = None,
14
+ ) -> Path:
15
+ """Set up logging configuration.
16
+
17
+ Args:
18
+ verbosity: Level of verbosity (0=WARNING, 1=INFO, 2=DEBUG)
19
+ log_file: Path to log file. If None, a temporary file is created.
20
+
21
+ Returns:
22
+ Path to the log file used.
23
+ """
24
+ # clear existing handlers
25
+ root_logger = logging.getLogger()
26
+ root_logger.handlers.clear()
27
+
28
+ # Determine log level for console
29
+ if verbosity >= 2:
30
+ console_level = logging.DEBUG
31
+ elif verbosity >= 1:
32
+ console_level = logging.INFO
33
+ else:
34
+ console_level = logging.WARNING
35
+
36
+ # Console handler (Rich)
37
+ console_handler = RichHandler(
38
+ rich_tracebacks=True,
39
+ markup=True,
40
+ show_time=False,
41
+ show_path=False,
42
+ )
43
+ console_handler.setLevel(console_level)
44
+
45
+ # File handler
46
+ if log_file is None:
47
+ # Create temp file if not specified
48
+ tf = tempfile.NamedTemporaryFile(
49
+ prefix="convoviz_",
50
+ suffix=".log",
51
+ delete=False
52
+ )
53
+ log_file = Path(tf.name)
54
+ tf.close()
55
+
56
+ # Ensure parent dir exists
57
+ if not log_file.parent.exists():
58
+ log_file.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
61
+ file_handler.setLevel(logging.DEBUG) # Always log DEBUG to file
62
+ file_formatter = logging.Formatter(
63
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
64
+ )
65
+ file_handler.setFormatter(file_formatter)
66
+
67
+ # Configure root logger
68
+ # We set root level to DEBUG so that the handlers can filter as they please
69
+ root_logger.setLevel(logging.DEBUG)
70
+ root_logger.addHandler(console_handler)
71
+ root_logger.addHandler(file_handler)
72
+
73
+ # Reduce noise from explicit libraries if necessary
74
+ logging.getLogger("matplotlib").setLevel(logging.WARNING)
75
+ logging.getLogger("PIL").setLevel(logging.WARNING)
76
+
77
+ return log_file
@@ -2,6 +2,7 @@
2
2
 
3
3
  from pathlib import Path
4
4
  from shutil import rmtree
5
+ import logging
5
6
 
6
7
  from rich.console import Console
7
8
 
@@ -15,6 +16,7 @@ from convoviz.io.loaders import (
15
16
  from convoviz.io.writers import save_collection
16
17
 
17
18
  console = Console()
19
+ logger = logging.getLogger(__name__)
18
20
 
19
21
 
20
22
  def _safe_uri(path: Path) -> str:
@@ -46,6 +48,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
46
48
  if not input_path.exists():
47
49
  raise InvalidZipError(str(input_path), reason="File does not exist")
48
50
 
51
+ logger.info(f"Starting pipeline with input: {input_path}")
49
52
  console.print(f"Loading data from {input_path} [bold yellow]📂[/bold yellow] ...\n")
50
53
 
51
54
  # Load collection based on input type
@@ -62,6 +65,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
62
65
  else:
63
66
  # Assume zip
64
67
  collection = load_collection_from_zip(input_path)
68
+ logger.info(f"Loaded collection with {len(collection.conversations)} conversations")
65
69
 
66
70
  # Try to merge bookmarklet data if available
67
71
  bookmarklet_json = find_latest_bookmarklet_json()
@@ -70,6 +74,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
70
74
  try:
71
75
  bookmarklet_collection = load_collection_from_json(bookmarklet_json)
72
76
  collection.update(bookmarklet_collection)
77
+ logger.info("Merged bookmarklet data")
73
78
  except Exception as e:
74
79
  console.print(
75
80
  f"[bold yellow]Warning:[/bold yellow] Failed to load bookmarklet data: {e}"
@@ -114,6 +119,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
114
119
  folder_organization=config.folder_organization,
115
120
  progress_bar=True,
116
121
  )
122
+ logger.info("Markdown generation complete")
117
123
  console.print(
118
124
  f"\nDone [bold green]✅[/bold green] ! "
119
125
  f"Check the output [bold blue]📄[/bold blue] here: {_safe_uri(markdown_folder)} 🔗\n"
@@ -138,6 +144,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
138
144
  config.graph,
139
145
  progress_bar=True,
140
146
  )
147
+ logger.info("Graph generation complete")
141
148
  console.print(
142
149
  f"\nDone [bold green]✅[/bold green] ! "
143
150
  f"Check the output [bold blue]📈[/bold blue] here: {_safe_uri(graph_folder)} 🔗\n"
@@ -162,6 +169,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
162
169
  config.wordcloud,
163
170
  progress_bar=True,
164
171
  )
172
+ logger.info("Wordcloud generation complete")
165
173
  console.print(
166
174
  f"\nDone [bold green]✅[/bold green] ! "
167
175
  f"Check the output [bold blue]🔡☁️[/bold blue] here: {_safe_uri(wordcloud_folder)} 🔗\n"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "convoviz"
3
- version = "0.3.2"
3
+ version = "0.3.9"
4
4
  description = "Get analytics and visualizations on your ChatGPT data!"
5
5
  license = "MIT"
6
6
  keywords = [
File without changes
File without changes
File without changes
File without changes
File without changes