notionary 0.2.15__tar.gz → 0.2.16__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.
- {notionary-0.2.15 → notionary-0.2.16}/PKG-INFO +2 -1
- {notionary-0.2.15 → notionary-0.2.16}/notionary/base_notion_client.py +1 -1
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/registry/block_registry.py +27 -3
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/notion_database.py +29 -15
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/notion_database_provider.py +2 -6
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/notion_page.py +1 -3
- notionary-0.2.16/notionary/telemetry/__init__.py +19 -0
- notionary-0.2.16/notionary/telemetry/service.py +136 -0
- notionary-0.2.16/notionary/telemetry/views.py +64 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/__init__.py +2 -0
- {notionary-0.2.15 → notionary-0.2.16}/pyproject.toml +3 -1
- {notionary-0.2.15 → notionary-0.2.16}/LICENSE +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/README.md +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/__init__.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/__init__.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/audio_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/bookmark_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/bulleted_list_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/callout_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/code_block_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/column_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/divider_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/embed_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/heading_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/image_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/mention_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/notion_block_client.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/notion_block_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/numbered_list_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/paragraph_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/prompts/element_prompt_builder.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/prompts/element_prompt_content.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/qoute_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/registry/block_registry_builder.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/table_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/text_inline_formatter.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/todo_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/toggle_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/toggleable_heading_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/blocks/video_element.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/__init__.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/client.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/database_exceptions.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/factory.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/filter_builder.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/database/models/page_result.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/elements/__init__.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/models/notion_block_response.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/models/notion_database_response.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/models/notion_page_response.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/models/search_response.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/__init__.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/client.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/content/notion_page_content_chunker.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/content/page_content_retriever.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/content/page_content_writer.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/formatting/markdown_to_notion_converter.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/formatting/spacer_rules.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/markdown_syntax_prompt_generator.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/notion_to_markdown_converter.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/properites/property_value_extractor.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/property_formatter.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/search_filter_builder.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/page/utils.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/factory_decorator.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/fuzzy_matcher.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/logging_mixin.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/page_id_utils.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/singleton_decorator.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/util/singleton_metaclass.py +0 -0
- {notionary-0.2.15 → notionary-0.2.16}/notionary/workspace.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: notionary
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.16
|
4
4
|
Summary: Python library for programmatic Notion workspace management - databases, pages, and content with advanced Markdown support
|
5
5
|
License: MIT
|
6
6
|
Author: Mathis Arends
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
16
16
|
Requires-Dist: httpx (>=0.28.0)
|
17
|
+
Requires-Dist: posthog (>=6.3.1,<7.0.0)
|
17
18
|
Requires-Dist: pydantic (>=2.11.4)
|
18
19
|
Requires-Dist: python-dotenv (>=1.1.0)
|
19
20
|
Project-URL: Homepage, https://github.com/mathisarends/notionary
|
@@ -194,7 +194,7 @@ class BaseNotionClient(LoggingMixin, ABC):
|
|
194
194
|
token = next(
|
195
195
|
(
|
196
196
|
os.getenv(var)
|
197
|
-
for var in ("NOTION_SECRET", "
|
197
|
+
for var in ("NOTION_SECRET", "NOTION_INTEGRATION_KEY", "NOTION_TOKEN")
|
198
198
|
if os.getenv(var)
|
199
199
|
),
|
200
200
|
None,
|
@@ -8,6 +8,12 @@ from notionary.page.markdown_syntax_prompt_generator import (
|
|
8
8
|
from notionary.blocks.text_inline_formatter import TextInlineFormatter
|
9
9
|
|
10
10
|
from notionary.blocks import NotionBlockElement
|
11
|
+
from notionary.telemetry import (
|
12
|
+
ProductTelemetry,
|
13
|
+
NotionMarkdownSyntaxPromptEvent,
|
14
|
+
MarkdownToNotionConversionEvent,
|
15
|
+
NotionToMarkdownConversionEvent,
|
16
|
+
)
|
11
17
|
|
12
18
|
|
13
19
|
class BlockRegistry:
|
@@ -28,6 +34,8 @@ class BlockRegistry:
|
|
28
34
|
for element in elements:
|
29
35
|
self.register(element)
|
30
36
|
|
37
|
+
self.telemetry = ProductTelemetry()
|
38
|
+
|
31
39
|
def register(self, element_class: Type[NotionBlockElement]) -> bool:
|
32
40
|
"""
|
33
41
|
Register an element class.
|
@@ -57,13 +65,13 @@ class BlockRegistry:
|
|
57
65
|
|
58
66
|
def contains(self, element_class: Type[NotionBlockElement]) -> bool:
|
59
67
|
"""
|
60
|
-
|
68
|
+
Checks if a specific element is contained in the registry.
|
61
69
|
|
62
70
|
Args:
|
63
|
-
element_class:
|
71
|
+
element_class: The element class to check.
|
64
72
|
|
65
73
|
Returns:
|
66
|
-
bool: True
|
74
|
+
bool: True if the element is contained, otherwise False.
|
67
75
|
"""
|
68
76
|
return element_class in self._elements
|
69
77
|
|
@@ -77,14 +85,28 @@ class BlockRegistry:
|
|
77
85
|
def markdown_to_notion(self, text: str) -> Optional[Dict[str, Any]]:
|
78
86
|
"""Convert markdown to Notion block using registered elements."""
|
79
87
|
handler = self.find_markdown_handler(text)
|
88
|
+
|
80
89
|
if handler:
|
90
|
+
self.telemetry.capture(
|
91
|
+
MarkdownToNotionConversionEvent(
|
92
|
+
handler_element_name=handler.__name__,
|
93
|
+
)
|
94
|
+
)
|
95
|
+
|
81
96
|
return handler.markdown_to_notion(text)
|
82
97
|
return None
|
83
98
|
|
84
99
|
def notion_to_markdown(self, block: Dict[str, Any]) -> Optional[str]:
|
85
100
|
"""Convert Notion block to markdown using registered elements."""
|
86
101
|
handler = self._find_notion_handler(block)
|
102
|
+
|
87
103
|
if handler:
|
104
|
+
self.telemetry.capture(
|
105
|
+
NotionToMarkdownConversionEvent(
|
106
|
+
handler_element_name=handler.__name__,
|
107
|
+
)
|
108
|
+
)
|
109
|
+
|
88
110
|
return handler.notion_to_markdown(block)
|
89
111
|
return None
|
90
112
|
|
@@ -106,6 +128,8 @@ class BlockRegistry:
|
|
106
128
|
if "TextInlineFormatter" not in formatter_names:
|
107
129
|
element_classes = element_classes + [TextInlineFormatter]
|
108
130
|
|
131
|
+
self.telemetry.capture(NotionMarkdownSyntaxPromptEvent())
|
132
|
+
|
109
133
|
return MarkdownSyntaxPromptGenerator.generate_system_prompt(element_classes)
|
110
134
|
|
111
135
|
def _find_notion_handler(
|
@@ -13,6 +13,11 @@ from notionary.page.notion_page import NotionPage
|
|
13
13
|
from notionary.database.notion_database_provider import NotionDatabaseProvider
|
14
14
|
|
15
15
|
from notionary.database.filter_builder import FilterBuilder
|
16
|
+
from notionary.telemetry import (
|
17
|
+
ProductTelemetry,
|
18
|
+
DatabaseFactoryUsedEvent,
|
19
|
+
QueryOperationEvent,
|
20
|
+
)
|
16
21
|
from notionary.util import factory_only, LoggingMixin
|
17
22
|
|
18
23
|
|
@@ -22,6 +27,8 @@ class NotionDatabase(LoggingMixin):
|
|
22
27
|
Focused exclusively on creating basic pages and retrieving page managers
|
23
28
|
for further page operations.
|
24
29
|
"""
|
30
|
+
|
31
|
+
telemetry = ProductTelemetry()
|
25
32
|
|
26
33
|
@factory_only("from_database_id", "from_database_name")
|
27
34
|
def __init__(
|
@@ -52,6 +59,10 @@ class NotionDatabase(LoggingMixin):
|
|
52
59
|
Create a NotionDatabase from a database ID using NotionDatabaseProvider.
|
53
60
|
"""
|
54
61
|
provider = cls.get_database_provider()
|
62
|
+
cls.telemetry.capture(
|
63
|
+
DatabaseFactoryUsedEvent(factory_method="from_database_id")
|
64
|
+
)
|
65
|
+
|
55
66
|
return await provider.get_database_by_id(id, token)
|
56
67
|
|
57
68
|
@classmethod
|
@@ -65,6 +76,9 @@ class NotionDatabase(LoggingMixin):
|
|
65
76
|
Create a NotionDatabase by finding a database with fuzzy matching on the title using NotionDatabaseProvider.
|
66
77
|
"""
|
67
78
|
provider = cls.get_database_provider()
|
79
|
+
cls.telemetry.capture(
|
80
|
+
DatabaseFactoryUsedEvent(factory_method="from_database_name")
|
81
|
+
)
|
68
82
|
return await provider.get_database_by_name(database_name, token, min_similarity)
|
69
83
|
|
70
84
|
@property
|
@@ -129,9 +143,7 @@ class NotionDatabase(LoggingMixin):
|
|
129
143
|
|
130
144
|
self._title = result.title[0].plain_text
|
131
145
|
self.logger.info(f"Successfully updated database title to: {new_title}")
|
132
|
-
self.database_provider.invalidate_database_cache(
|
133
|
-
database_id=self.id
|
134
|
-
)
|
146
|
+
self.database_provider.invalidate_database_cache(database_id=self.id)
|
135
147
|
return True
|
136
148
|
|
137
149
|
except Exception as e:
|
@@ -149,9 +161,7 @@ class NotionDatabase(LoggingMixin):
|
|
149
161
|
|
150
162
|
self._emoji_icon = result.icon.emoji if result.icon else None
|
151
163
|
self.logger.info(f"Successfully updated database emoji to: {new_emoji}")
|
152
|
-
self.database_provider.invalidate_database_cache(
|
153
|
-
database_id=self.id
|
154
|
-
)
|
164
|
+
self.database_provider.invalidate_database_cache(database_id=self.id)
|
155
165
|
return True
|
156
166
|
|
157
167
|
except Exception as e:
|
@@ -168,9 +178,7 @@ class NotionDatabase(LoggingMixin):
|
|
168
178
|
)
|
169
179
|
|
170
180
|
if result.cover and result.cover.external:
|
171
|
-
self.database_provider.invalidate_database_cache(
|
172
|
-
database_id=self.id
|
173
|
-
)
|
181
|
+
self.database_provider.invalidate_database_cache(database_id=self.id)
|
174
182
|
return result.cover.external.url
|
175
183
|
return None
|
176
184
|
|
@@ -197,9 +205,7 @@ class NotionDatabase(LoggingMixin):
|
|
197
205
|
)
|
198
206
|
|
199
207
|
if result.icon and result.icon.external:
|
200
|
-
self.database_provider.invalidate_database_cache(
|
201
|
-
database_id=self.id
|
202
|
-
)
|
208
|
+
self.database_provider.invalidate_database_cache(database_id=self.id)
|
203
209
|
return result.icon.external.url
|
204
210
|
return None
|
205
211
|
|
@@ -251,9 +257,15 @@ class NotionDatabase(LoggingMixin):
|
|
251
257
|
page_results: List[NotionPage] = []
|
252
258
|
|
253
259
|
for page in search_results.results:
|
254
|
-
page = NotionPage.from_page_id(
|
260
|
+
page = await NotionPage.from_page_id(
|
261
|
+
page_id=page.id, token=self.client.token
|
262
|
+
)
|
255
263
|
page_results.append(page)
|
256
264
|
|
265
|
+
self.telemetry.capture(
|
266
|
+
QueryOperationEvent(query_type="query_database_by_title")
|
267
|
+
)
|
268
|
+
|
257
269
|
return page_results
|
258
270
|
|
259
271
|
async def iter_pages_updated_within(
|
@@ -289,7 +301,7 @@ class NotionDatabase(LoggingMixin):
|
|
289
301
|
ISO 8601 timestamp string of the last database edit, or None if request fails.
|
290
302
|
"""
|
291
303
|
try:
|
292
|
-
db = await self.client.get_database(self.
|
304
|
+
db = await self.client.get_database(self.id)
|
293
305
|
|
294
306
|
return db.last_edited_time
|
295
307
|
|
@@ -352,7 +364,9 @@ class NotionDatabase(LoggingMixin):
|
|
352
364
|
return
|
353
365
|
|
354
366
|
for page in result.results:
|
355
|
-
yield await NotionPage.from_page_id(
|
367
|
+
yield await NotionPage.from_page_id(
|
368
|
+
page_id=page.id, token=self.client.token
|
369
|
+
)
|
356
370
|
|
357
371
|
has_more = result.has_more
|
358
372
|
start_cursor = result.next_cursor if has_more else None
|
@@ -57,18 +57,14 @@ class NotionDatabaseProvider(LoggingMixin, metaclass=SingletonMetaClass):
|
|
57
57
|
|
58
58
|
id_cache_key = self._create_id_cache_key(database.id)
|
59
59
|
if not force_refresh and id_cache_key in self._database_cache:
|
60
|
-
self.logger.debug(
|
61
|
-
f"Found existing cached database by ID: {database.id}"
|
62
|
-
)
|
60
|
+
self.logger.debug(f"Found existing cached database by ID: {database.id}")
|
63
61
|
existing_database = self._database_cache[id_cache_key]
|
64
62
|
|
65
63
|
self._database_cache[name_cache_key] = existing_database
|
66
64
|
return existing_database
|
67
65
|
|
68
66
|
self._cache_database(database, token, database_name)
|
69
|
-
self.logger.debug(
|
70
|
-
f"Cached database: {database.title} (ID: {database.id})"
|
71
|
-
)
|
67
|
+
self.logger.debug(f"Cached database: {database.title} (ID: {database.id})")
|
72
68
|
|
73
69
|
return database
|
74
70
|
|
@@ -500,9 +500,7 @@ class NotionPage(LoggingMixin):
|
|
500
500
|
parent_database_id = cls._extract_parent_database_id(page_response)
|
501
501
|
|
502
502
|
parent_database = (
|
503
|
-
await NotionDatabase.from_database_id(
|
504
|
-
id=parent_database_id, token=token
|
505
|
-
)
|
503
|
+
await NotionDatabase.from_database_id(id=parent_database_id, token=token)
|
506
504
|
if parent_database_id
|
507
505
|
else None
|
508
506
|
)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from .service import ProductTelemetry
|
2
|
+
from .views import (
|
3
|
+
BaseTelemetryEvent,
|
4
|
+
DatabaseFactoryUsedEvent,
|
5
|
+
QueryOperationEvent,
|
6
|
+
NotionMarkdownSyntaxPromptEvent,
|
7
|
+
MarkdownToNotionConversionEvent,
|
8
|
+
NotionToMarkdownConversionEvent,
|
9
|
+
)
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"ProductTelemetry",
|
13
|
+
"BaseTelemetryEvent",
|
14
|
+
"DatabaseFactoryUsedEvent",
|
15
|
+
"QueryOperationEvent",
|
16
|
+
"NotionMarkdownSyntaxPromptEvent",
|
17
|
+
"MarkdownToNotionConversionEvent",
|
18
|
+
"NotionToMarkdownConversionEvent",
|
19
|
+
]
|
@@ -0,0 +1,136 @@
|
|
1
|
+
import os
|
2
|
+
import uuid
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Dict, Any, Optional
|
5
|
+
from posthog import Posthog
|
6
|
+
from dotenv import load_dotenv
|
7
|
+
|
8
|
+
from notionary.telemetry.views import BaseTelemetryEvent
|
9
|
+
from notionary.util import SingletonMetaClass, LoggingMixin
|
10
|
+
|
11
|
+
load_dotenv()
|
12
|
+
|
13
|
+
POSTHOG_EVENT_SETTINGS = {
|
14
|
+
"process_person_profile": True,
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
class ProductTelemetry(LoggingMixin, metaclass=SingletonMetaClass):
|
19
|
+
"""
|
20
|
+
Anonymous telemetry for Notionary - enabled by default.
|
21
|
+
Disable via: ANONYMIZED_NOTIONARY_TELEMETRY=false
|
22
|
+
"""
|
23
|
+
|
24
|
+
USER_ID_PATH = str(Path.home() / ".cache" / "notionary" / "telemetry_user_id")
|
25
|
+
PROJECT_API_KEY = "phc_gItKOx21Tc0l07C1taD0QPpqFnbWgWjVfRjF6z24kke"
|
26
|
+
HOST = "https://eu.i.posthog.com"
|
27
|
+
UNKNOWN_USER_ID = "UNKNOWN"
|
28
|
+
|
29
|
+
_logged_init_message = False
|
30
|
+
_curr_user_id = None
|
31
|
+
|
32
|
+
def __init__(self):
|
33
|
+
# Default: enabled, disable via environment variable
|
34
|
+
telemetry_setting = os.getenv("ANONYMIZED_NOTIONARY_TELEMETRY", "true").lower()
|
35
|
+
telemetry_disabled = telemetry_setting == "false"
|
36
|
+
self.debug_logging = os.getenv("NOTIONARY_DEBUG", "false").lower() == "true"
|
37
|
+
|
38
|
+
if telemetry_disabled:
|
39
|
+
self._posthog_client = None
|
40
|
+
else:
|
41
|
+
if not self._logged_init_message:
|
42
|
+
self.logger.info(
|
43
|
+
"Anonymous telemetry enabled to improve Notionary. "
|
44
|
+
"To disable: export ANONYMIZED_NOTIONARY_TELEMETRY=false"
|
45
|
+
)
|
46
|
+
self._logged_init_message = True
|
47
|
+
|
48
|
+
self._posthog_client = Posthog(
|
49
|
+
project_api_key=self.PROJECT_API_KEY,
|
50
|
+
host=self.HOST,
|
51
|
+
disable_geoip=True,
|
52
|
+
enable_exception_autocapture=True,
|
53
|
+
)
|
54
|
+
|
55
|
+
# Silence posthog's logging unless debug mode
|
56
|
+
if not self.debug_logging:
|
57
|
+
import logging
|
58
|
+
|
59
|
+
posthog_logger = logging.getLogger("posthog")
|
60
|
+
posthog_logger.disabled = True
|
61
|
+
|
62
|
+
if self._posthog_client is None:
|
63
|
+
self.logger.debug("Telemetry disabled")
|
64
|
+
|
65
|
+
def capture(self, event: BaseTelemetryEvent) -> None:
|
66
|
+
"""
|
67
|
+
Safe event tracking that never affects library functionality
|
68
|
+
|
69
|
+
Args:
|
70
|
+
event: BaseTelemetryEvent instance to capture
|
71
|
+
"""
|
72
|
+
if self._posthog_client is None:
|
73
|
+
return
|
74
|
+
|
75
|
+
self._direct_capture(event)
|
76
|
+
|
77
|
+
def _direct_capture(self, event: BaseTelemetryEvent) -> None:
|
78
|
+
"""
|
79
|
+
Direct capture method - PostHog handles threading internally
|
80
|
+
Should not be thread blocking because posthog magically handles it
|
81
|
+
"""
|
82
|
+
if self._posthog_client is None:
|
83
|
+
return
|
84
|
+
|
85
|
+
try:
|
86
|
+
self._posthog_client.capture(
|
87
|
+
distinct_id=self.user_id,
|
88
|
+
event=event.name,
|
89
|
+
properties={
|
90
|
+
"library": "notionary",
|
91
|
+
**event.properties,
|
92
|
+
**POSTHOG_EVENT_SETTINGS,
|
93
|
+
},
|
94
|
+
)
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
self.logger.error(f"Failed to send telemetry event {event.name}: {e}")
|
98
|
+
|
99
|
+
def flush(self) -> None:
|
100
|
+
"""
|
101
|
+
Flush pending events - simplified without threading complexity
|
102
|
+
"""
|
103
|
+
if not self._posthog_client:
|
104
|
+
self.logger.debug("PostHog client not available, skipping flush.")
|
105
|
+
return
|
106
|
+
|
107
|
+
try:
|
108
|
+
self._posthog_client.flush()
|
109
|
+
self.logger.debug("PostHog client telemetry queue flushed.")
|
110
|
+
except Exception as e:
|
111
|
+
self.logger.error(f"Failed to flush PostHog client: {e}")
|
112
|
+
|
113
|
+
@property
|
114
|
+
def user_id(self) -> str:
|
115
|
+
"""Anonymous, persistent user ID"""
|
116
|
+
if self._curr_user_id:
|
117
|
+
return self._curr_user_id
|
118
|
+
|
119
|
+
# File access may fail due to permissions or other reasons.
|
120
|
+
# We don't want to crash so we catch all exceptions.
|
121
|
+
try:
|
122
|
+
if not os.path.exists(self.USER_ID_PATH):
|
123
|
+
os.makedirs(os.path.dirname(self.USER_ID_PATH), exist_ok=True)
|
124
|
+
with open(self.USER_ID_PATH, "w") as f:
|
125
|
+
new_user_id = str(uuid.uuid4())
|
126
|
+
f.write(new_user_id)
|
127
|
+
self._curr_user_id = new_user_id
|
128
|
+
else:
|
129
|
+
with open(self.USER_ID_PATH, "r") as f:
|
130
|
+
self._curr_user_id = f.read().strip()
|
131
|
+
|
132
|
+
return self._curr_user_id
|
133
|
+
except Exception as e:
|
134
|
+
self.logger.debug(f"Error getting user ID: {e}")
|
135
|
+
self._curr_user_id = self.UNKNOWN_USER_ID
|
136
|
+
return self._curr_user_id
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from dataclasses import asdict, dataclass
|
3
|
+
from typing import Any, Optional
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class BaseTelemetryEvent(ABC):
|
8
|
+
@property
|
9
|
+
@abstractmethod
|
10
|
+
def name(self) -> str:
|
11
|
+
pass
|
12
|
+
|
13
|
+
@property
|
14
|
+
def properties(self) -> dict[str, Any]:
|
15
|
+
return {k: v for k, v in asdict(self).items() if k != "name"}
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class DatabaseFactoryUsedEvent(BaseTelemetryEvent):
|
20
|
+
"""Event fired when a database factory method is used"""
|
21
|
+
|
22
|
+
factory_method: str
|
23
|
+
|
24
|
+
@property
|
25
|
+
def name(self) -> str:
|
26
|
+
return "database_factory_used"
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class QueryOperationEvent(BaseTelemetryEvent):
|
30
|
+
"""Event fired when a query operation is performed"""
|
31
|
+
|
32
|
+
query_type: str
|
33
|
+
|
34
|
+
@property
|
35
|
+
def name(self) -> str:
|
36
|
+
return "query_operation"
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class NotionMarkdownSyntaxPromptEvent(BaseTelemetryEvent):
|
40
|
+
"""Event fired when Notion Markdown syntax is used"""
|
41
|
+
|
42
|
+
@property
|
43
|
+
def name(self) -> str:
|
44
|
+
return "notion_markdown_syntax_used"
|
45
|
+
|
46
|
+
# Tracks markdown conversion
|
47
|
+
@dataclass
|
48
|
+
class MarkdownToNotionConversionEvent(BaseTelemetryEvent):
|
49
|
+
"""Event fired when markdown is converted to Notion blocks"""
|
50
|
+
handler_element_name: Optional[str] = None # e.g. "HeadingElement", "ParagraphElement"
|
51
|
+
|
52
|
+
@property
|
53
|
+
def name(self) -> str:
|
54
|
+
return "markdown_to_notion_conversion"
|
55
|
+
|
56
|
+
|
57
|
+
@dataclass
|
58
|
+
class NotionToMarkdownConversionEvent(BaseTelemetryEvent):
|
59
|
+
"""Event fired when Notion blocks are converted to markdown"""
|
60
|
+
handler_element_name: Optional[str] = None # e.g. "HeadingElement", "ParagraphElement"
|
61
|
+
|
62
|
+
@property
|
63
|
+
def name(self) -> str:
|
64
|
+
return "notion_to_markdown_conversion"
|
@@ -3,6 +3,7 @@ from .singleton_decorator import singleton
|
|
3
3
|
from .page_id_utils import format_uuid
|
4
4
|
from .fuzzy_matcher import FuzzyMatcher
|
5
5
|
from .factory_decorator import factory_only
|
6
|
+
from .singleton_metaclass import SingletonMetaClass
|
6
7
|
|
7
8
|
__all__ = [
|
8
9
|
"LoggingMixin",
|
@@ -11,4 +12,5 @@ __all__ = [
|
|
11
12
|
"FuzzyMatcher",
|
12
13
|
"factory_only",
|
13
14
|
"singleton",
|
15
|
+
"SingletonMetaClass",
|
14
16
|
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "notionary"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.16"
|
4
4
|
description = "Python library for programmatic Notion workspace management - databases, pages, and content with advanced Markdown support"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.9"
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
12
12
|
"httpx>=0.28.0",
|
13
13
|
"python-dotenv>=1.1.0",
|
14
14
|
"pydantic>=2.11.4",
|
15
|
+
"posthog (>=6.3.1,<7.0.0)",
|
15
16
|
]
|
16
17
|
|
17
18
|
[project.urls]
|
@@ -24,4 +25,5 @@ build-backend = "poetry.core.masonry.api"
|
|
24
25
|
[tool.poetry.group.dev.dependencies]
|
25
26
|
mkdocs = "^1.6.1"
|
26
27
|
mkdocs-material = "^9.6.15"
|
28
|
+
black = "^25.1.0"
|
27
29
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{notionary-0.2.15 → notionary-0.2.16}/notionary/page/formatting/markdown_to_notion_converter.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|