notionary 0.2.12__py3-none-any.whl → 0.2.13__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.
Files changed (35) hide show
  1. notionary/__init__.py +0 -4
  2. notionary/cli/main.py +169 -6
  3. notionary/cli/onboarding.py +116 -0
  4. notionary/database/notion_database.py +0 -2
  5. notionary/database/notion_database_factory.py +0 -3
  6. notionary/elements/audio_element.py +1 -2
  7. notionary/elements/bookmark_element.py +1 -2
  8. notionary/elements/bulleted_list_element.py +1 -2
  9. notionary/elements/callout_element.py +1 -2
  10. notionary/elements/code_block_element.py +1 -2
  11. notionary/elements/column_element.py +1 -2
  12. notionary/elements/divider_element.py +1 -2
  13. notionary/elements/embed_element.py +1 -2
  14. notionary/elements/heading_element.py +1 -2
  15. notionary/elements/image_element.py +1 -2
  16. notionary/elements/mention_element.py +1 -2
  17. notionary/elements/notion_block_element.py +1 -36
  18. notionary/elements/numbered_list_element.py +1 -2
  19. notionary/elements/paragraph_element.py +1 -2
  20. notionary/elements/qoute_element.py +1 -2
  21. notionary/elements/table_element.py +1 -2
  22. notionary/elements/todo_element.py +1 -2
  23. notionary/elements/toggle_element.py +1 -2
  24. notionary/elements/toggleable_heading_element.py +1 -2
  25. notionary/elements/video_element.py +1 -2
  26. notionary/page/notion_page_factory.py +0 -4
  27. {notionary-0.2.12.dist-info → notionary-0.2.13.dist-info}/METADATA +1 -1
  28. {notionary-0.2.12.dist-info → notionary-0.2.13.dist-info}/RECORD +32 -35
  29. notionary/telemetry/__init__.py +0 -7
  30. notionary/telemetry/telemetry.py +0 -226
  31. notionary/telemetry/track_usage_decorator.py +0 -76
  32. {notionary-0.2.12.dist-info → notionary-0.2.13.dist-info}/WHEEL +0 -0
  33. {notionary-0.2.12.dist-info → notionary-0.2.13.dist-info}/entry_points.txt +0 -0
  34. {notionary-0.2.12.dist-info → notionary-0.2.13.dist-info}/licenses/LICENSE +0 -0
  35. {notionary-0.2.12.dist-info → notionary-0.2.13.dist-info}/top_level.txt +0 -0
notionary/__init__.py CHANGED
@@ -14,10 +14,6 @@ from .elements.registry.block_registry_builder import (
14
14
  BlockRegistryBuilder,
15
15
  )
16
16
 
17
- from .telemetry.telemetry import NotionaryTelemetry
18
-
19
- _telemetry = NotionaryTelemetry()
20
-
21
17
  __all__ = [
22
18
  "NotionClient",
23
19
  "NotionDatabase",
notionary/cli/main.py CHANGED
@@ -6,11 +6,35 @@ Notionary CLI - Integration Key Setup
6
6
  import click
7
7
  import os
8
8
  import platform
9
+ import asyncio
10
+ import logging
9
11
  from pathlib import Path
10
12
  from dotenv import load_dotenv
11
13
  from rich.console import Console
12
14
  from rich.panel import Panel
13
15
  from rich.prompt import Prompt, Confirm
16
+ from rich.table import Table
17
+ from rich import box
18
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
19
+ from notionary.notion_client import NotionClient
20
+ from notionary.database.database_discovery import DatabaseDiscovery
21
+
22
+ # Disable logging for CLI usage
23
+ def disable_notionary_logging():
24
+ """Disable logging for notionary modules when used in CLI"""
25
+ # Option 1: Set to WARNING level (recommended for CLI)
26
+ logging.getLogger('notionary').setLevel(logging.WARNING)
27
+ logging.getLogger('DatabaseDiscovery').setLevel(logging.WARNING)
28
+ logging.getLogger('NotionClient').setLevel(logging.WARNING)
29
+
30
+ def enable_verbose_logging():
31
+ """Enable verbose logging for debugging (use with --verbose flag)"""
32
+ logging.getLogger('notionary').setLevel(logging.DEBUG)
33
+ logging.getLogger('DatabaseDiscovery').setLevel(logging.DEBUG)
34
+ logging.getLogger('NotionClient').setLevel(logging.DEBUG)
35
+
36
+ # Initialize logging configuration for CLI
37
+ disable_notionary_logging()
14
38
 
15
39
  console = Console()
16
40
 
@@ -46,12 +70,126 @@ def get_notion_secret() -> str:
46
70
  load_dotenv()
47
71
  return os.getenv("NOTION_SECRET", "")
48
72
 
73
+ async def fetch_notion_databases_with_progress():
74
+ """Fetch databases using DatabaseDiscovery with progress animation"""
75
+ try:
76
+ # Initialize NotionClient and DatabaseDiscovery
77
+ client = NotionClient()
78
+ discovery = DatabaseDiscovery(client)
79
+
80
+ # Create progress display with custom spinner
81
+ with Progress(
82
+ SpinnerColumn(spinner_name="dots12", style="cyan"),
83
+ TextColumn("[bold blue]Discovering databases..."),
84
+ TimeElapsedColumn(),
85
+ console=console,
86
+ transient=True
87
+ ) as progress:
88
+ # Add progress task
89
+ task = progress.add_task("Fetching...", total=None)
90
+
91
+ # Fetch databases
92
+ databases = await discovery._discover(page_size=50)
93
+
94
+ # Update progress to show completion
95
+ progress.update(task, description=f"[bold green]Found {len(databases)} databases!")
96
+
97
+ # Brief pause to show completion
98
+ await asyncio.sleep(0.5)
99
+
100
+ return {"databases": databases, "success": True}
101
+
102
+ except Exception as e:
103
+ return {"error": str(e), "success": False}
104
+
105
+ def show_databases_overview(api_key: str):
106
+ """Show available databases with nice formatting"""
107
+ console.print("\n[bold blue]🔍 Connecting to Notion...[/bold blue]")
108
+
109
+ # Run async function in sync context
110
+ try:
111
+ result = asyncio.run(fetch_notion_databases_with_progress())
112
+ except Exception as e:
113
+ console.print(Panel.fit(
114
+ f"[bold red]❌ Unexpected error[/bold red]\n\n"
115
+ f"[red]{str(e)}[/red]\n\n"
116
+ "[yellow]Please check:[/yellow]\n"
117
+ "• Your internet connection\n"
118
+ "• Your integration key validity\n"
119
+ "• Try running the command again",
120
+ title="Connection Error"
121
+ ))
122
+ return
123
+
124
+ if not result["success"]:
125
+ console.print(Panel.fit(
126
+ f"[bold red]❌ Could not fetch databases[/bold red]\n\n"
127
+ f"[red]{result['error']}[/red]\n\n"
128
+ "[yellow]Common issues:[/yellow]\n"
129
+ "• Check your integration key\n"
130
+ "• Make sure your integration has access to databases\n"
131
+ "• Visit your integration settings to grant access",
132
+ title="Connection Error"
133
+ ))
134
+ return
135
+
136
+ databases = result["databases"]
137
+
138
+ if not databases:
139
+ console.print(Panel.fit(
140
+ "[bold yellow]⚠️ No databases found[/bold yellow]\n\n"
141
+ "Your integration key is valid, but no databases are accessible.\n\n"
142
+ "[bold blue]To grant access:[/bold blue]\n"
143
+ "1. Go to any Notion database\n"
144
+ "2. Click the '...' menu (top right)\n"
145
+ "3. Go to 'Add connections'\n"
146
+ "4. Find and select your integration\n\n"
147
+ "[cyan]https://www.notion.so/help/add-and-manage-connections-with-the-api[/cyan]",
148
+ title="No Databases Available"
149
+ ))
150
+ return
151
+
152
+ # Create beautiful table
153
+ table = Table(
154
+ title=f"📊 Available Databases ({len(databases)} found)",
155
+ box=box.ROUNDED,
156
+ title_style="bold green",
157
+ header_style="bold cyan"
158
+ )
159
+
160
+ table.add_column("#", style="dim", justify="right", width=3)
161
+ table.add_column("Database Name", style="bold white", min_width=25)
162
+ table.add_column("ID", style="dim cyan", min_width=36)
163
+
164
+ for i, (title, db_id) in enumerate(databases, 1):
165
+ table.add_row(
166
+ str(i),
167
+ title or "Untitled Database",
168
+ db_id
169
+ )
170
+
171
+ console.print("\n")
172
+ console.print(table)
173
+
174
+ # Success message with next steps
175
+ console.print(Panel.fit(
176
+ "[bold green]🎉 Setup Complete![/bold green]\n\n"
177
+ f"Found [bold cyan]{len(databases)}[/bold cyan] accessible database(s).\n"
178
+ "You can now use notionary in your Python code!\n\n"
179
+ "[bold yellow]💡 Tip:[/bold yellow] Run [cyan]notionary db[/cyan] anytime to see this overview again.",
180
+ title="Ready to Go!"
181
+ ))
182
+
49
183
  @click.group()
50
184
  @click.version_option() # Automatische Version aus setup.py
51
- def main():
185
+ @click.option('--verbose', '-v', is_flag=True, help='Enable verbose logging')
186
+ def main(verbose):
52
187
  """
53
188
  Notionary CLI - Notion API Integration
54
189
  """
190
+ if verbose:
191
+ enable_verbose_logging()
192
+ console.print("[dim]Verbose logging enabled[/dim]")
55
193
  pass
56
194
 
57
195
  @main.command()
@@ -70,11 +208,19 @@ def init():
70
208
  title="Already Configured"
71
209
  ))
72
210
 
73
- # Option to reconfigure
74
- if Confirm.ask("\n[yellow]Would you like to update your key?[/yellow]"):
211
+ # Option to reconfigure or show databases
212
+ choice = Prompt.ask(
213
+ "\n[yellow]What would you like to do?[/yellow]",
214
+ choices=["show", "update", "exit"],
215
+ default="show"
216
+ )
217
+
218
+ if choice == "show":
219
+ show_databases_overview(existing_key)
220
+ elif choice == "update":
75
221
  setup_new_key()
76
222
  else:
77
- console.print("\n[blue]No changes made. Happy coding! 🚀[/blue]")
223
+ console.print("\n[blue]Happy coding! 🚀[/blue]")
78
224
  else:
79
225
  # No key found, start setup
80
226
  console.print(Panel.fit(
@@ -86,6 +232,23 @@ def init():
86
232
  ))
87
233
  setup_new_key()
88
234
 
235
+ @main.command()
236
+ def db() -> None:
237
+ """
238
+ Show available Notion databases
239
+ """
240
+ existing_key = get_notion_secret()
241
+
242
+ if not existing_key:
243
+ console.print(Panel.fit(
244
+ "[bold red]❌ No Integration Key found![/bold red]\n\n"
245
+ "Please run [cyan]notionary init[/cyan] first to set up your key.",
246
+ title="Not Configured"
247
+ ))
248
+ return
249
+
250
+ show_databases_overview(existing_key)
251
+
89
252
  def setup_new_key():
90
253
  """Handle the key setup process"""
91
254
  try:
@@ -126,7 +289,8 @@ def setup_new_key():
126
289
 
127
290
  # Save the key
128
291
  if save_integration_key(integration_key):
129
- return # Success!
292
+ # Show databases overview after successful setup
293
+ show_databases_overview(integration_key)
130
294
 
131
295
  except KeyboardInterrupt:
132
296
  console.print("\n[yellow]Setup cancelled.[/yellow]")
@@ -170,7 +334,6 @@ def save_integration_key(integration_key: str) -> bool:
170
334
  if written_key == integration_key:
171
335
  console.print("\n[bold green]✅ Integration Key saved and verified![/bold green]")
172
336
  console.print(f"[dim]Configuration: {env_file}[/dim]")
173
- console.print("\n[blue]Ready to use notionary in your Python code! 🚀[/blue]")
174
337
  return True
175
338
  else:
176
339
  console.print("\n[bold red]❌ Error: Key verification failed![/bold red]")
@@ -0,0 +1,116 @@
1
+ import asyncio
2
+ from dataclasses import dataclass
3
+ from notionary import NotionDatabase
4
+
5
+ @dataclass
6
+ class OnboardingPageResult:
7
+ url: str
8
+ tile: str
9
+ emoji: str
10
+
11
+ async def generate_doc_for_database(
12
+ datbase_name: str,
13
+ ) -> OnboardingPageResult:
14
+ database = await NotionDatabase.from_database_name(datbase_name)
15
+ page = await database.create_blank_page()
16
+
17
+ page_title = "Welcome to Notionary!"
18
+ page_icon = "📚"
19
+
20
+ markdown_content = """!> [🚀] This page was created fully automatically and serves as a showcase of what is possible with Notionary.
21
+
22
+ ---
23
+
24
+ ## 🗃️ Working with Databases
25
+
26
+ Discover and manage your Notion databases programmatically:
27
+
28
+ ```python
29
+ import asyncio
30
+ from notionary import NotionDatabase, DatabaseDiscovery
31
+
32
+ async def main():
33
+ # Discover available databases
34
+ discovery = DatabaseDiscovery()
35
+ await discovery()
36
+
37
+ # Connect to a database by name
38
+ db = await NotionDatabase.from_database_name("Projects")
39
+
40
+ # Create a new page in the database
41
+ page = await db.create_blank_page()
42
+
43
+ # Query pages from database
44
+ async for page in db.iter_pages():
45
+ title = await page.get_title()
46
+ print(f"Page: {title}")
47
+
48
+ if __name__ == "__main__":
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ ## 📄 Creating and Managing Pages
53
+ Create and update Notion pages with rich content:
54
+ ```python
55
+ import asyncio
56
+ from notionary import NotionPage
57
+
58
+ async def main():
59
+ # Create a page from URL
60
+ page = NotionPage.from_url("https://www.notion.so/your-page-url")
61
+
62
+ # Or find by name
63
+ page = await NotionPage.from_page_name("My Project Page")
64
+
65
+ # Update page metadata
66
+ await page.set_title("Updated Title")
67
+ await page.set_emoji_icon("🚀")
68
+ await page.set_random_gradient_cover()
69
+
70
+ # Add markdown content
71
+ markdown = '''
72
+ # Project Overview
73
+
74
+ !> [💡] This page was created programmatically using Notionary.
75
+
76
+ ## Features
77
+ - **Rich** Markdown support
78
+ - Async functionality
79
+ - Custom syntax extensions
80
+ '''
81
+
82
+ await page.replace_content(markdown)
83
+
84
+ if __name__ == "__main__":
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ ## 📊 Tables and Structured Data
89
+ Create tables for organizing information:
90
+ FeatureStatusPriorityAPI IntegrationCompleteHighDocumentationIn ProgressMediumDatabase QueriesCompleteHighFile UploadsCompleteMedium
91
+
92
+ 🎥 Media Embedding
93
+ Embed videos directly in your pages:
94
+ @[Caption](https://www.youtube.com/watch?v=dQw4w9WgXcQ) - Never gonna give you up!
95
+
96
+ Happy building with Notionary! 🎉"""
97
+
98
+
99
+ await page.set_title(page_title)
100
+ await page.set_emoji_icon(page_icon)
101
+ await page.set_random_gradient_cover()
102
+ await page.append_markdown(markdown_content)
103
+
104
+ url = await page.get_url()
105
+
106
+ return OnboardingPageResult(
107
+ url=url,
108
+ tile=page_title,
109
+ emoji=page_icon,
110
+ )
111
+
112
+
113
+ if __name__ == "__main__":
114
+ print("🚀 Starting Notionary onboarding page generation...")
115
+ result = asyncio.run(generate_doc_for_database("Wissen & Notizen"))
116
+ print(f"✅ Onboarding page created: {result.tile} {result.emoji} - {result.url}")
@@ -3,7 +3,6 @@ from typing import Any, AsyncGenerator, Dict, List, Optional
3
3
 
4
4
  from notionary.notion_client import NotionClient
5
5
  from notionary.page.notion_page import NotionPage
6
- from notionary.telemetry import NotionaryTelemetry
7
6
  from notionary.util.warn_direct_constructor_usage import warn_direct_constructor_usage
8
7
  from notionary.util import LoggingMixin
9
8
  from notionary.util.page_id_utils import format_uuid
@@ -26,7 +25,6 @@ class NotionDatabase(LoggingMixin):
26
25
  token: Optional Notion API token
27
26
  """
28
27
  self.database_id = database_id
29
- self._telemetry = NotionaryTelemetry()
30
28
  self._client = NotionClient(token=token)
31
29
 
32
30
  @classmethod
@@ -10,7 +10,6 @@ from notionary.exceptions.database_exceptions import (
10
10
  DatabaseParsingError,
11
11
  NotionDatabaseException,
12
12
  )
13
- from notionary.telemetry import track_usage
14
13
  from notionary.util import LoggingMixin
15
14
  from notionary.util import format_uuid
16
15
  from notionary.util import singleton
@@ -24,7 +23,6 @@ class NotionDatabaseFactory(LoggingMixin):
24
23
  """
25
24
 
26
25
  @classmethod
27
- @track_usage('page_factory_method_used', {'method': 'from_page_id'})
28
26
  def from_database_id(
29
27
  cls, database_id: str, token: Optional[str] = None
30
28
  ) -> NotionDatabase:
@@ -59,7 +57,6 @@ class NotionDatabaseFactory(LoggingMixin):
59
57
  raise DatabaseConnectionError(error_msg) from e
60
58
 
61
59
  @classmethod
62
- @track_usage('page_factory_method_used', {'method': 'from_url'})
63
60
  async def from_database_name(
64
61
  cls, database_name: str, token: Optional[str] = None
65
62
  ) -> NotionDatabase:
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
- @auto_track_conversions
10
9
  class AudioElement(NotionBlockElement):
11
10
  """
12
11
  Handles conversion between Markdown audio embeds and Notion audio blocks.
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
3
 
4
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
 
10
- @auto_track_conversions
11
10
  class BookmarkElement(NotionBlockElement):
12
11
  """
13
12
  Handles conversion between Markdown bookmarks and Notion bookmark blocks.
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
- @auto_track_conversions
11
10
  class BulletedListElement(NotionBlockElement):
12
11
  """Class for converting between Markdown bullet lists and Notion bulleted list items."""
13
12
 
@@ -6,9 +6,8 @@ from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
9
+ from notionary.elements.notion_block_element import NotionBlockElement
10
10
 
11
- @auto_track_conversions
12
11
  class CalloutElement(NotionBlockElement):
13
12
  """
14
13
  Handles conversion between Markdown callouts and Notion callout blocks.
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
- @auto_track_conversions
10
9
  class CodeBlockElement(NotionBlockElement):
11
10
  """
12
11
  Handles conversion between Markdown code blocks and Notion code blocks.
@@ -1,14 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple, Callable
3
3
 
4
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
5
  from notionary.page.formatting.spacer_rules import SPACER_MARKER
6
6
  from notionary.prompting.element_prompt_content import (
7
7
  ElementPromptBuilder,
8
8
  ElementPromptContent,
9
9
  )
10
10
 
11
- @auto_track_conversions
12
11
  class ColumnElement(NotionBlockElement):
13
12
  """
14
13
  Handles conversion between custom Markdown column syntax and Notion column blocks.
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
3
 
4
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
 
10
- @auto_track_conversions
11
10
  class DividerElement(NotionBlockElement):
12
11
  """
13
12
  Handles conversion between Markdown horizontal dividers and Notion divider blocks.
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
- @auto_track_conversions
10
9
  class EmbedElement(NotionBlockElement):
11
10
  """
12
11
  Handles conversion between Markdown embeds and Notion embed blocks.
@@ -1,14 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
3
 
4
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
  from notionary.elements.text_inline_formatter import TextInlineFormatter
10
10
 
11
- @auto_track_conversions
12
11
  class HeadingElement(NotionBlockElement):
13
12
  """Handles conversion between Markdown headings and Notion heading blocks."""
14
13
 
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
- @auto_track_conversions
10
9
  class ImageElement(NotionBlockElement):
11
10
  """
12
11
  Handles conversion between Markdown images and Notion image blocks.
@@ -2,13 +2,12 @@ import re
2
2
  from typing import Dict, Any, Optional, List
3
3
  from typing_extensions import override
4
4
 
5
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
5
+ from notionary.elements.notion_block_element import NotionBlockElement
6
6
  from notionary.prompting.element_prompt_content import (
7
7
  ElementPromptBuilder,
8
8
  ElementPromptContent,
9
9
  )
10
10
 
11
- @auto_track_conversions
12
11
  class MentionElement(NotionBlockElement):
13
12
  """
14
13
  Handles conversion between Markdown mentions and Notion mention elements.
@@ -3,7 +3,6 @@ from typing import Dict, Any, Optional
3
3
  from abc import ABC
4
4
 
5
5
  from notionary.prompting.element_prompt_content import ElementPromptContent
6
- from notionary.telemetry import track_usage
7
6
 
8
7
 
9
8
  class NotionBlockElement(ABC):
@@ -33,38 +32,4 @@ class NotionBlockElement(ABC):
33
32
 
34
33
  @classmethod
35
34
  def get_llm_prompt_content(cls) -> ElementPromptContent:
36
- """Returns a dictionary with information for LLM prompts about this element."""
37
-
38
-
39
- def auto_track_conversions(cls):
40
- """
41
- Decorator der sich auch auf Subklassen vererbt.
42
- """
43
- conversion_methods = ['markdown_to_notion', 'notion_to_markdown']
44
-
45
- original_init_subclass = getattr(cls, '__init_subclass__', None)
46
-
47
- @classmethod
48
- def __init_subclass__(cls_inner, **kwargs):
49
- # Original __init_subclass__ aufrufen
50
- if original_init_subclass:
51
- original_init_subclass(**kwargs)
52
-
53
- # Tracking für Subklasse hinzufügen
54
- for method_name in conversion_methods:
55
- if hasattr(cls_inner, method_name):
56
- original_method = getattr(cls_inner, method_name)
57
-
58
- if isinstance(original_method, classmethod):
59
- func = original_method.__func__
60
-
61
- @track_usage(f"{cls_inner.__name__.lower()}_{method_name}")
62
- @classmethod
63
- @wraps(func)
64
- def tracked_method(cls_ref, *args, **kwargs):
65
- return func(cls_ref, *args, **kwargs)
66
-
67
- setattr(cls_inner, method_name, tracked_method)
68
-
69
- cls.__init_subclass__ = __init_subclass__
70
- return cls
35
+ """Returns a dictionary with information for LLM prompts about this element."""
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
- @auto_track_conversions
11
10
  class NumberedListElement(NotionBlockElement):
12
11
  """Class for converting between Markdown numbered lists and Notion numbered list items."""
13
12
 
@@ -1,13 +1,12 @@
1
1
  from typing import Dict, Any, Optional
2
2
 
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
- @auto_track_conversions
11
10
  class ParagraphElement(NotionBlockElement):
12
11
  """Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
13
12
 
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
- @auto_track_conversions
10
9
  class QuoteElement(NotionBlockElement):
11
10
  """Class for converting between Markdown blockquotes and Notion quote blocks."""
12
11
 
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.elements.text_inline_formatter import TextInlineFormatter
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
 
10
- @auto_track_conversions
11
10
  class TableElement(NotionBlockElement):
12
11
  """
13
12
  Handles conversion between Markdown tables and Notion table blocks.
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
- @auto_track_conversions
11
10
  class TodoElement(NotionBlockElement):
12
11
  """
13
12
  Handles conversion between Markdown todo items and Notion to_do blocks.
@@ -1,13 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple, Callable
3
3
 
4
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
 
10
- @auto_track_conversions
11
10
  class ToggleElement(NotionBlockElement):
12
11
  """
13
12
  Improved ToggleElement class using pipe syntax instead of indentation.
@@ -1,14 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple, Callable
3
3
 
4
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
+ from notionary.elements.notion_block_element import NotionBlockElement
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
  from notionary.elements.text_inline_formatter import TextInlineFormatter
10
10
 
11
- @auto_track_conversions
12
11
  class ToggleableHeadingElement(NotionBlockElement):
13
12
  """Handles conversion between Markdown collapsible headings and Notion toggleable heading blocks with pipe syntax."""
14
13
 
@@ -1,12 +1,11 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
- from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
3
+ from notionary.elements.notion_block_element import NotionBlockElement
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
- @auto_track_conversions
10
9
  class VideoElement(NotionBlockElement):
11
10
  """
12
11
  Handles conversion between Markdown video embeds and Notion video blocks.
@@ -2,7 +2,6 @@ from typing import List, Optional, Dict, Any, Tuple
2
2
  from difflib import SequenceMatcher
3
3
 
4
4
  from notionary import NotionPage, NotionClient
5
- from notionary.telemetry import track_usage
6
5
  from notionary.util import LoggingMixin
7
6
  from notionary.util import format_uuid, extract_and_validate_page_id
8
7
  from notionary.util import singleton
@@ -20,7 +19,6 @@ class NotionPageFactory(LoggingMixin):
20
19
  EARLY_STOP_THRESHOLD = 0.95
21
20
 
22
21
  @classmethod
23
- @track_usage('page_factory_method_used', {'method': 'from_page_id'})
24
22
  def from_page_id(cls, page_id: str, token: Optional[str] = None) -> NotionPage:
25
23
  """Create a NotionPage from a page ID."""
26
24
 
@@ -36,7 +34,6 @@ class NotionPageFactory(LoggingMixin):
36
34
  raise
37
35
 
38
36
  @classmethod
39
- @track_usage('page_factory_method_used', {'method': 'from_url'})
40
37
  def from_url(cls, url: str, token: Optional[str] = None) -> NotionPage:
41
38
  """Create a NotionPage from a Notion URL."""
42
39
 
@@ -56,7 +53,6 @@ class NotionPageFactory(LoggingMixin):
56
53
  raise
57
54
 
58
55
  @classmethod
59
- @track_usage('page_factory_method_used', {'method': 'from_page_name'})
60
56
  async def from_page_name(
61
57
  cls, page_name: str, token: Optional[str] = None
62
58
  ) -> NotionPage:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.2.12
3
+ Version: 0.2.13
4
4
  Summary: A toolkit to convert between Markdown and Notion blocks
5
5
  Home-page: https://github.com/mathisarends/notionary
6
6
  Author: Mathis Arends
@@ -1,32 +1,32 @@
1
- notionary/__init__.py,sha256=I4kPJ_LEy_IIQuBVGZ-U_kiIUWeRUP9DZ_F3w9BvS-4,809
1
+ notionary/__init__.py,sha256=U7I4nffaEt1Gfg6N7TFupC_GQVZttLhW5u0qjVX7gP4,717
2
2
  notionary/notion_client.py,sha256=gkREAr8LkUUKK9cOvq72r8jNjlXDleBP2fYm7LjjbjM,7311
3
- notionary/cli/main.py,sha256=GmUfdrJZjHYo689ZhrMuA4Lr_sBE2YeqrkFloXgrfvY,6663
4
- notionary/cli/onboarding.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ notionary/cli/main.py,sha256=0tmX9y_xJksKPV8evfUIxp2MTKsHAvNcq76CW4PZCSs,12836
4
+ notionary/cli/onboarding.py,sha256=I7G6eaEw_WWmbfBKBa4nHgItGyZg6iwtEw9pZylsqa4,3376
5
5
  notionary/database/database_discovery.py,sha256=l9IjthwdA_Y_k_JXcAW-KnvZDwNYylIbsrQ5cpgtb5w,4484
6
- notionary/database/notion_database.py,sha256=yTGMK1EOuJDNiP6boc_dvWDLTTEhOOYa_ZdJWiOP19c,7695
7
- notionary/database/notion_database_factory.py,sha256=pOu4MDu7jaSkNQHk-TSEezsJ1pPGJXiSTOouygh2JuY,6783
6
+ notionary/database/notion_database.py,sha256=vbMu8FRao0nXRkSK68Q6637ypiikyMpP2zOmzGWsgpU,7595
7
+ notionary/database/notion_database_factory.py,sha256=6AK9c63R8qyQX46VY-eNL1DufHe6pK_Erka0QEBVO90,6594
8
8
  notionary/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
9
- notionary/elements/audio_element.py,sha256=wXSo0DQs_KIVdLB-Ax5Yk4puW-_gFwAbJHcgcpwZjoA,5392
10
- notionary/elements/bookmark_element.py,sha256=bDJEMTLC1E3qUIjphFJC9l60ZAeJqZOWEvS__Y5Iq00,8192
11
- notionary/elements/bulleted_list_element.py,sha256=Ax6tIGtc92O57QKVWRKi2Sgkbmd6KqkSmssTcj17Tjg,2711
12
- notionary/elements/callout_element.py,sha256=w45gJogoqf4Z9aVqSq7toaU06IWWM_et5dCWywj_oeo,4539
13
- notionary/elements/code_block_element.py,sha256=PrsV58fzblmZz0vRkuAG2TT0kpiMglq0UlKaR-B-dJw,7576
14
- notionary/elements/column_element.py,sha256=yfWSLEClcxrWLteQClabaIvjay2h8krB6u5rfbnBRBI,12781
15
- notionary/elements/divider_element.py,sha256=bdGTgMgW8Uk6Fu7oy2UBrDaQ_2XiLzPNwF3JmrBS00I,2379
16
- notionary/elements/embed_element.py,sha256=QYyiKTbAM6COPqe61a3jSg6m2a4orUpA6l37lrn4-G0,4626
17
- notionary/elements/heading_element.py,sha256=XDO_hgiG-J2nVF3hwcpvsXte6uGDAAwyHx661lD-1H8,3215
18
- notionary/elements/image_element.py,sha256=agV8ZNwoJNOIvaJNARs2osdbe1T5MHPNbrzu7L19uRI,4805
19
- notionary/elements/mention_element.py,sha256=-8GbeaiGNglw5MZulomSa-hUh32HaZowAROeCgzMqdA,8274
20
- notionary/elements/notion_block_element.py,sha256=cou_87mAkj7U3LLU0kXJrbxVl8VusnlVBYYb4cchOPo,2603
21
- notionary/elements/numbered_list_element.py,sha256=iiDzjOwc5Fnv2RQsaTLeOtRVQdshG4rRQJa4LMJjCvY,2710
22
- notionary/elements/paragraph_element.py,sha256=EWUac9oq5zXso0svE5vTKNWlpq6sRdJxxVWYMEmYLz4,3302
23
- notionary/elements/qoute_element.py,sha256=cIa7S0G9Kb4qR5EXWOLJGPn0IzWm1zLujnzzO6t4-D0,6169
24
- notionary/elements/table_element.py,sha256=acIEoDz8tblj9pPmE5Gr0Ov-S9qJOTkEDNcUPBfsN4I,11296
9
+ notionary/elements/audio_element.py,sha256=m1zMUYkpQNPCx9S0KpU85ps7pnm9Pzu4lglKYzOozF4,5343
10
+ notionary/elements/bookmark_element.py,sha256=M_vGJfBVVhUDa7sDfHB622m1Q9wEn4Lw0pCmuDkxpvQ,8143
11
+ notionary/elements/bulleted_list_element.py,sha256=dyWNu28l_fG6fob5zQzMadnWb5g_B2n7ixCHwaniPdE,2662
12
+ notionary/elements/callout_element.py,sha256=OVaRLdxNFXTJnMzmfg3SZClOAYfJ4oORSr_Skm5EOPA,4490
13
+ notionary/elements/code_block_element.py,sha256=hBMn3VpFJLgq12llvdSFkjLX05poMCIZWsBaUF9ZOQA,7527
14
+ notionary/elements/column_element.py,sha256=VpPe2yVvozMFIladOMxMh0Q1nsND_XysYxpxWoIH8eU,12732
15
+ notionary/elements/divider_element.py,sha256=sqfs1YRVqsEFlKdBOX9p8K2Ise2lDb19HwcwHM_R7nU,2330
16
+ notionary/elements/embed_element.py,sha256=PkoaycRel6bAVkP1XHv57vuLVwFmzgeZYKqM5AT8Lg0,4577
17
+ notionary/elements/heading_element.py,sha256=zgqC6alKoaucpeWDpDJd-TIsbMoJzNI5_BGP-D8Xbsc,3166
18
+ notionary/elements/image_element.py,sha256=rd9P9BT9YUwofO24JadH7bMtQCJLxVHdjHF1_ykGS-g,4756
19
+ notionary/elements/mention_element.py,sha256=-AVZ8rn6y-S9Paw-vUDZpa8nhn0dR5L_0qNjd9dQI_s,8225
20
+ notionary/elements/notion_block_element.py,sha256=utWHp_JZSRQgNIWiZfJfzwl7y2ALuDqGTY-Ve3OoQeg,1285
21
+ notionary/elements/numbered_list_element.py,sha256=kgJfQQ5FIKZVRjygd6sLJKkE9-IDlx-MFAEaxWVsg84,2661
22
+ notionary/elements/paragraph_element.py,sha256=oOIRkknpEEAw5Pr81D6c-L6cGRvaZGtggLT3iVYGC4Q,3253
23
+ notionary/elements/qoute_element.py,sha256=SCvNWht-38EwMIsDg19VPajKNc1IhZBP3oVxLQw_yVA,6120
24
+ notionary/elements/table_element.py,sha256=6yTY0fNT_Ae2ncwGQE0CSsfCxGNvnDblTTzd3kdz-Sw,11247
25
25
  notionary/elements/text_inline_formatter.py,sha256=q1WePwTxhSkjhTFylcyAbhxaWLo_sjYS3Q_mIPgsKb4,8625
26
- notionary/elements/todo_element.py,sha256=r7H1KcEW7Iq6pQrhsPRBwHn_3ok26tR2fJf53U_kr5I,4171
27
- notionary/elements/toggle_element.py,sha256=caPT8wAo7ccwT2pzUzySrImnUlVB2cigFBGe32vwjJk,11129
28
- notionary/elements/toggleable_heading_element.py,sha256=mUzR1HrkOJEgzjIgVgtInvLoi4YzsPJ0pR3FO_OlD5U,10007
29
- notionary/elements/video_element.py,sha256=Y2JOvb4fovnY2nPAtTPSbdtk-HaRdWqg6RXr-_a2WOw,5774
26
+ notionary/elements/todo_element.py,sha256=BKw3KvjdDsZIlSXoqV52z9R-a3KBzkH-9DKo4dN7m9w,4122
27
+ notionary/elements/toggle_element.py,sha256=sg3LfblBRa0pIrcSBHPRknq6FYWB5-mpDGvA7PBOZ9U,11080
28
+ notionary/elements/toggleable_heading_element.py,sha256=sKvjD_x61QYOM4l8gfq6VodpLWqxf8FR2JGWXNeee08,9958
29
+ notionary/elements/video_element.py,sha256=IlB88CmBueYPSFh6p7kxE5zVjcZBQJJ1K953G7kg99M,5725
30
30
  notionary/elements/registry/block_registry.py,sha256=g0id_Q6guzTyNY6HfnB9AjOBvCR4CvtpnUeFAY8kgY0,5027
31
31
  notionary/elements/registry/block_registry_builder.py,sha256=5zRKnw2102rAeHpANs6Csu4DVufOazf1peEovChWcgs,9572
32
32
  notionary/exceptions/database_exceptions.py,sha256=I-Tx6bYRLpi5pjGPtbT-Mqxvz3BFgYTiuZxknJeLxtI,2638
@@ -35,7 +35,7 @@ notionary/models/notion_block_response.py,sha256=gzL4C6K9QPcaMS6NbAZaRceSEnMbNwY
35
35
  notionary/models/notion_database_response.py,sha256=FMAasQP20S12J_KMdMlNpcHHwxFKX2YtbE4Q9xn-ruQ,1213
36
36
  notionary/models/notion_page_response.py,sha256=r4fwMwwDocj92JdbSmyrzIqBKsnEaz4aDUiPabrg9BM,1762
37
37
  notionary/page/notion_page.py,sha256=CnEr5S425t7r8n4mZERwShlXsXnR2G7bbYjO8yb2oaU,18032
38
- notionary/page/notion_page_factory.py,sha256=p75yhr6FdYl9aG8Ttpf5pyuQiMJB0CAYMkfcVLI8HP0,12028
38
+ notionary/page/notion_page_factory.py,sha256=_dsvxn3xmjZFQw3fKIhnTwiIQfk7p8pxVOFj1GMvBZc,11763
39
39
  notionary/page/notion_to_markdown_converter.py,sha256=vUQss0J7LUFLULGvW27PjaTFuWi8OsRQAUBowSYorkM,6408
40
40
  notionary/page/content/notion_page_content_chunker.py,sha256=kWJnV9GLU5YLgSVPKOjwMBbG_CMAmVRkuDtwJYb_UAA,3316
41
41
  notionary/page/content/page_content_retriever.py,sha256=MoRNwVyBacQEPFu-XseahKEFait0q8tjuhFUXHOBrMo,2208
@@ -54,17 +54,14 @@ notionary/page/relations/notion_page_title_resolver.py,sha256=LN89y-Tc0Rk81TiTeA
54
54
  notionary/page/relations/page_database_relation.py,sha256=nkelofYzfuIFjmM7vR6IGJpWUG9XPmSDnU1WR8WtQrs,2231
55
55
  notionary/prompting/element_prompt_content.py,sha256=tHref-SKA81Ua_IQD2Km7y7BvFtHl74haSIjHNYE3FE,4403
56
56
  notionary/prompting/markdown_syntax_prompt_generator.py,sha256=_1qIYlqSfI6q6Fut10t6gGwTQuS8c3QBcC_5DBme9Mo,5084
57
- notionary/telemetry/__init__.py,sha256=qdFGrhOhUb_HPMLS4kLSr1BJNfb5-LzHwLmx-NYCDsw,156
58
- notionary/telemetry/telemetry.py,sha256=SEwEsegNhX7xblKnlk9qCDqKOTtf8sGtnNWbLiOVk7g,8182
59
- notionary/telemetry/track_usage_decorator.py,sha256=rmxwySb1PfZI1mXXU0G_W9Fpn6ByslYrDjsU4JYnMJ4,2421
60
57
  notionary/util/__init__.py,sha256=ra1jHFFiQNWYDzmVb81OVhtshzkZ0GcLVI8YDODYj3w,235
61
58
  notionary/util/logging_mixin.py,sha256=d5sRSmUtgQeuckdNBkO025IXPGe4oOb-7ueVAIP8amU,1846
62
59
  notionary/util/page_id_utils.py,sha256=EYNMxgf-7ghzL5K8lKZBZfW7g5CsdY0Xuj4IYmU8RPk,1381
63
60
  notionary/util/singleton.py,sha256=CKAvykndwPRZsA3n3MAY_XdCR59MBjjKP0vtm2BcvF0,428
64
61
  notionary/util/warn_direct_constructor_usage.py,sha256=vyJR73F95XVSRWIbyij-82IGOpAne9SBPM25eDpZfSU,1715
65
- notionary-0.2.12.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
66
- notionary-0.2.12.dist-info/METADATA,sha256=vZNayJWGNmJAJBfitVjQXYEkJC2YjJRE0fmEK6s3Aeg,7582
67
- notionary-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
- notionary-0.2.12.dist-info/entry_points.txt,sha256=V7X21u3QNm7h7p6Cx0Sx2SO3mtmA7gVwXM8lNYnv9fk,54
69
- notionary-0.2.12.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
70
- notionary-0.2.12.dist-info/RECORD,,
62
+ notionary-0.2.13.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
63
+ notionary-0.2.13.dist-info/METADATA,sha256=jXcnz9Gqxx8vFbnLdrLhr_TiByxrWd_OMIxPPqyqHGY,7582
64
+ notionary-0.2.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ notionary-0.2.13.dist-info/entry_points.txt,sha256=V7X21u3QNm7h7p6Cx0Sx2SO3mtmA7gVwXM8lNYnv9fk,54
66
+ notionary-0.2.13.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
67
+ notionary-0.2.13.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- from .telemetry import NotionaryTelemetry
2
- from .track_usage_decorator import track_usage
3
-
4
- __all__ = [
5
- "NotionaryTelemetry",
6
- "track_usage",
7
- ]
@@ -1,226 +0,0 @@
1
- import os
2
- import uuid
3
- import atexit
4
- import signal
5
- import threading
6
- from pathlib import Path
7
- from typing import Dict, Any, Optional
8
- from posthog import Posthog
9
- from dotenv import load_dotenv
10
-
11
- from notionary.util import LoggingMixin
12
- from notionary.util import singleton
13
-
14
- load_dotenv()
15
-
16
- @singleton
17
- class NotionaryTelemetry(LoggingMixin):
18
- """
19
- Anonymous telemetry for Notionary - enabled by default.
20
- Disable via: ANONYMIZED_TELEMETRY=false
21
- """
22
-
23
- USER_ID_PATH = str(Path.home() / ".cache" / "notionary" / "telemetry_user_id")
24
- PROJECT_API_KEY = (
25
- "phc_gItKOx21Tc0l07C1taD0QPpqFnbWgWjVfRjF6z24kke" # write-only so no worries
26
- )
27
- HOST = "https://eu.i.posthog.com"
28
-
29
- _logged_init_message = False
30
-
31
- def __init__(self):
32
- # Default: enabled, disable via ANONYMIZED_TELEMETRY=false
33
- telemetry_setting = os.getenv("ANONYMIZED_TELEMETRY", "true").lower()
34
- self.enabled = telemetry_setting != "false"
35
-
36
- self._user_id = None
37
- self._client = None
38
- self._shutdown_lock = threading.Lock()
39
- self._is_shutdown = False
40
- self._shutdown_registered = False
41
-
42
- if self.enabled:
43
- self._initialize_client()
44
- self._register_shutdown_handlers()
45
-
46
- def _register_shutdown_handlers(self):
47
- """Register shutdown handlers for clean exit"""
48
- with self._shutdown_lock:
49
- if self._shutdown_registered:
50
- return
51
-
52
- try:
53
- # Register atexit handler for normal program termination
54
- atexit.register(self._atexit_handler)
55
-
56
- # Register signal handlers for SIGINT (Ctrl+C) and SIGTERM
57
- signal.signal(signal.SIGINT, self._signal_handler)
58
- signal.signal(signal.SIGTERM, self._signal_handler)
59
-
60
- self._shutdown_registered = True
61
- self.logger.debug("Telemetry shutdown handlers registered")
62
-
63
- except Exception as e:
64
- self.logger.debug(f"Failed to register shutdown handlers: {e}")
65
-
66
- def _signal_handler(self, signum, frame):
67
- """Handle SIGINT (Ctrl+C) and SIGTERM signals"""
68
- signal_name = "SIGINT" if signum == signal.SIGINT else f"SIG{signum}"
69
- self.logger.debug(f"Received {signal_name}, shutting down telemetry...")
70
-
71
- self.shutdown(timeout=5.0) # Quick shutdown for signals
72
-
73
- # Let the original signal handler take over (or exit)
74
- if signum == signal.SIGINT:
75
- # Restore default handler and re-raise
76
- signal.signal(signal.SIGINT, signal.SIG_DFL)
77
- os.kill(os.getpid(), signal.SIGINT)
78
-
79
- def _atexit_handler(self):
80
- """Handle normal program exit"""
81
- self.logger.debug("Normal program exit, shutting down telemetry...")
82
- self.shutdown(timeout=10.0)
83
-
84
- @property
85
- def user_id(self) -> str:
86
- """Anonymous, persistent user ID"""
87
- if self._user_id:
88
- return self._user_id
89
-
90
- try:
91
- if not os.path.exists(self.USER_ID_PATH):
92
- os.makedirs(os.path.dirname(self.USER_ID_PATH), exist_ok=True)
93
- with open(self.USER_ID_PATH, "w") as f:
94
- new_user_id = str(uuid.uuid4())
95
- f.write(new_user_id)
96
- self._user_id = new_user_id
97
- else:
98
- with open(self.USER_ID_PATH, "r") as f:
99
- self._user_id = f.read().strip()
100
-
101
- return self._user_id
102
- except Exception as e:
103
- self.logger.debug(f"Error getting user ID: {e}")
104
- return "anonymous_user"
105
-
106
- def capture(self, event_name: str, properties: Optional[Dict[str, Any]] = None):
107
- """
108
- Safe event tracking that never affects library functionality
109
-
110
- Args:
111
- event_name: Event name (e.g. 'page_factory_used')
112
- properties: Event properties as dictionary
113
- """
114
- if not self.enabled or not self._client or self._is_shutdown:
115
- return
116
-
117
- try:
118
- # Add base properties
119
- event_properties = {
120
- "library": "notionary",
121
- "library_version": self._get_notionary_version(),
122
- **(properties or {}),
123
- }
124
-
125
- self._client.capture(
126
- distinct_id=self.user_id, event=event_name, properties=event_properties
127
- )
128
-
129
- except Exception:
130
- pass
131
-
132
- def flush(self, timeout: float = 5.0):
133
- """
134
- Flush events with timeout
135
-
136
- Args:
137
- timeout: Maximum time to wait for flush to complete
138
- """
139
- if not self.enabled or not self._client or self._is_shutdown:
140
- return
141
-
142
- try:
143
- # PostHog flush doesn't support timeout directly, so we do it in a thread
144
- flush_thread = threading.Thread(target=self._client.flush)
145
- flush_thread.daemon = True
146
- flush_thread.start()
147
- flush_thread.join(timeout=timeout)
148
-
149
- if flush_thread.is_alive():
150
- self.logger.warning(f"Telemetry flush timed out after {timeout}s")
151
- else:
152
- self.logger.debug("Telemetry events flushed successfully")
153
-
154
- except Exception as e:
155
- self.logger.debug(f"Error during telemetry flush: {e}")
156
-
157
- def shutdown(self, timeout: float = 10.0):
158
- """
159
- Clean shutdown of telemetry with timeout
160
-
161
- Args:
162
- timeout: Maximum time to wait for shutdown
163
- """
164
- with self._shutdown_lock:
165
- if self._is_shutdown:
166
- return
167
-
168
- self._is_shutdown = True
169
-
170
- try:
171
- if self._client:
172
- # First try to flush remaining events
173
- self.logger.debug("Flushing telemetry events before shutdown...")
174
- self.flush(timeout=timeout * 0.7) # Use 70% of timeout for flush
175
-
176
- # Then shutdown the client
177
- shutdown_thread = threading.Thread(target=self._client.shutdown)
178
- shutdown_thread.daemon = True
179
- shutdown_thread.start()
180
- shutdown_thread.join(timeout=timeout * 0.3) # Use 30% for shutdown
181
-
182
- if shutdown_thread.is_alive():
183
- self.logger.warning(f"Telemetry client shutdown timed out after {timeout}s")
184
- else:
185
- self.logger.debug("Telemetry client shut down successfully")
186
-
187
- except Exception as e:
188
- self.logger.debug(f"Error during telemetry shutdown: {e}")
189
- finally:
190
- self._client = None
191
-
192
- def _initialize_client(self):
193
- """Initializes PostHog client and shows startup message"""
194
- try:
195
- self._client = Posthog(
196
- project_api_key=self.PROJECT_API_KEY,
197
- host=self.HOST,
198
- disable_geoip=True,
199
- )
200
- if not self._logged_init_message:
201
- self.logger.info(
202
- "Anonymous telemetry enabled to improve Notionary. "
203
- "To disable: export ANONYMIZED_TELEMETRY=false"
204
- )
205
- self._logged_init_message = True
206
-
207
- self._track_initialization()
208
-
209
- except Exception as e:
210
- self.logger.debug(f"Telemetry initialization failed: {e}")
211
- self.enabled = False
212
- self._client = None
213
-
214
- def _track_initialization(self):
215
- """Tracks library initialization"""
216
- self.capture(
217
- "notionary_initialized",
218
- {
219
- "version": self._get_notionary_version(),
220
- },
221
- )
222
-
223
- def _get_notionary_version(self) -> str:
224
- """Determines the Notionary version"""
225
- import notionary
226
- return getattr(notionary, "__version__", "0.2.10")
@@ -1,76 +0,0 @@
1
- from functools import wraps
2
- from typing import Any, Callable, Dict, Optional
3
- from notionary.telemetry import NotionaryTelemetry
4
-
5
-
6
- def track_usage(event_name: Optional[str] = None, properties: Optional[Dict[str, Any]] = None):
7
- """
8
- Simple decorator to track function usage.
9
-
10
- Args:
11
- event_name: Custom event name (defaults to function name)
12
- properties: Additional properties to track
13
-
14
- Usage:
15
- @track_usage()
16
- def my_function():
17
- pass
18
-
19
- @track_usage('custom_event_name')
20
- def my_function():
21
- pass
22
-
23
- @track_usage('custom_event', {'feature': 'advanced'})
24
- def my_function():
25
- pass
26
- """
27
- def decorator(func: Callable) -> Callable:
28
- @wraps(func)
29
- def wrapper(*args, **kwargs):
30
- telemetry = NotionaryTelemetry()
31
-
32
- # Generate event name and properties
33
- event = event_name or _generate_event_name(func, args)
34
- event_properties = _build_properties(func, args, properties)
35
-
36
- # Track and execute
37
- telemetry.capture(event, event_properties)
38
- return func(*args, **kwargs)
39
-
40
- return wrapper
41
- return decorator
42
-
43
-
44
- def _get_class_name(func: Callable, args: tuple) -> Optional[str]:
45
- """Extract class name from function or arguments."""
46
- if args and hasattr(args[0], '__class__'):
47
- return args[0].__class__.__name__
48
-
49
- if hasattr(func, '__qualname__') and '.' in func.__qualname__:
50
- return func.__qualname__.split('.')[0]
51
-
52
- return None
53
-
54
-
55
- def _generate_event_name(func: Callable, args: tuple) -> str:
56
- """Generate event name from function and class info."""
57
- class_name = _get_class_name(func, args)
58
-
59
- if class_name:
60
- return f"{class_name.lower()}_{func.__name__}_used"
61
-
62
- return f"{func.__name__}_used"
63
-
64
-
65
- def _build_properties(func: Callable, args: tuple, properties: Optional[Dict[str, Any]]) -> Dict[str, Any]:
66
- """Build event properties with function and class info."""
67
- event_properties = {
68
- 'function_name': func.__name__,
69
- **(properties or {})
70
- }
71
-
72
- class_name = _get_class_name(func, args)
73
- if class_name:
74
- event_properties['class_name'] = class_name
75
-
76
- return event_properties