notionary 0.2.14__py3-none-any.whl → 0.2.16__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.
- notionary/base_notion_client.py +1 -1
- notionary/blocks/registry/block_registry.py +27 -3
- notionary/database/notion_database.py +44 -30
- notionary/database/notion_database_provider.py +6 -10
- notionary/page/notion_page.py +1 -3
- notionary/telemetry/__init__.py +19 -0
- notionary/telemetry/service.py +136 -0
- notionary/telemetry/views.py +64 -0
- notionary/util/__init__.py +2 -0
- notionary-0.2.16.dist-info/METADATA +224 -0
- {notionary-0.2.14.dist-info → notionary-0.2.16.dist-info}/RECORD +22 -23
- {notionary-0.2.14.dist-info → notionary-0.2.16.dist-info}/WHEEL +1 -2
- notionary/cli/main.py +0 -376
- notionary/cli/onboarding.py +0 -117
- notionary-0.2.14.dist-info/METADATA +0 -276
- notionary-0.2.14.dist-info/entry_points.txt +0 -2
- notionary-0.2.14.dist-info/top_level.txt +0 -1
- {notionary-0.2.14.dist-info/licenses → notionary-0.2.16.dist-info}/LICENSE +0 -0
notionary/base_notion_client.py
CHANGED
@@ -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,11 +27,13 @@ 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__(
|
28
35
|
self,
|
29
|
-
|
36
|
+
id: str,
|
30
37
|
title: str,
|
31
38
|
url: str,
|
32
39
|
emoji_icon: Optional[str] = None,
|
@@ -36,7 +43,7 @@ class NotionDatabase(LoggingMixin):
|
|
36
43
|
"""
|
37
44
|
Initialize the minimal database manager.
|
38
45
|
"""
|
39
|
-
self.
|
46
|
+
self._id = id
|
40
47
|
self._title = title
|
41
48
|
self._url = url
|
42
49
|
self._emoji_icon = emoji_icon
|
@@ -46,13 +53,17 @@ class NotionDatabase(LoggingMixin):
|
|
46
53
|
|
47
54
|
@classmethod
|
48
55
|
async def from_database_id(
|
49
|
-
cls,
|
56
|
+
cls, id: str, token: Optional[str] = None
|
50
57
|
) -> NotionDatabase:
|
51
58
|
"""
|
52
59
|
Create a NotionDatabase from a database ID using NotionDatabaseProvider.
|
53
60
|
"""
|
54
61
|
provider = cls.get_database_provider()
|
55
|
-
|
62
|
+
cls.telemetry.capture(
|
63
|
+
DatabaseFactoryUsedEvent(factory_method="from_database_id")
|
64
|
+
)
|
65
|
+
|
66
|
+
return await provider.get_database_by_id(id, token)
|
56
67
|
|
57
68
|
@classmethod
|
58
69
|
async def from_database_name(
|
@@ -65,12 +76,15 @@ 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
|
71
|
-
def
|
85
|
+
def id(self) -> str:
|
72
86
|
"""Get the database ID (readonly)."""
|
73
|
-
return self.
|
87
|
+
return self._id
|
74
88
|
|
75
89
|
@property
|
76
90
|
def title(self) -> str:
|
@@ -109,7 +123,7 @@ class NotionDatabase(LoggingMixin):
|
|
109
123
|
"""
|
110
124
|
try:
|
111
125
|
create_page_response: NotionPageResponse = await self.client.create_page(
|
112
|
-
parent_database_id=self.
|
126
|
+
parent_database_id=self.id
|
113
127
|
)
|
114
128
|
|
115
129
|
return await NotionPage.from_page_id(page_id=create_page_response.id)
|
@@ -124,14 +138,12 @@ class NotionDatabase(LoggingMixin):
|
|
124
138
|
"""
|
125
139
|
try:
|
126
140
|
result = await self.client.update_database_title(
|
127
|
-
database_id=self.
|
141
|
+
database_id=self.id, title=new_title
|
128
142
|
)
|
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.database_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:
|
@@ -144,14 +156,12 @@ class NotionDatabase(LoggingMixin):
|
|
144
156
|
"""
|
145
157
|
try:
|
146
158
|
result = await self.client.update_database_emoji(
|
147
|
-
database_id=self.
|
159
|
+
database_id=self.id, emoji=new_emoji
|
148
160
|
)
|
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.database_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:
|
@@ -164,13 +174,11 @@ class NotionDatabase(LoggingMixin):
|
|
164
174
|
"""
|
165
175
|
try:
|
166
176
|
result = await self.client.update_database_cover_image(
|
167
|
-
database_id=self.
|
177
|
+
database_id=self.id, image_url=image_url
|
168
178
|
)
|
169
179
|
|
170
180
|
if result.cover and result.cover.external:
|
171
|
-
self.database_provider.invalidate_database_cache(
|
172
|
-
database_id=self.database_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
|
|
@@ -193,13 +201,11 @@ class NotionDatabase(LoggingMixin):
|
|
193
201
|
"""
|
194
202
|
try:
|
195
203
|
result = await self.client.update_database_external_icon(
|
196
|
-
database_id=self.
|
204
|
+
database_id=self.id, icon_url=external_icon_url
|
197
205
|
)
|
198
206
|
|
199
207
|
if result.icon and result.icon.external:
|
200
|
-
self.database_provider.invalidate_database_cache(
|
201
|
-
database_id=self.database_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
|
|
@@ -244,16 +250,22 @@ class NotionDatabase(LoggingMixin):
|
|
244
250
|
"""
|
245
251
|
search_results: NotionQueryDatabaseResponse = (
|
246
252
|
await self.client.query_database_by_title(
|
247
|
-
database_id=self.
|
253
|
+
database_id=self.id, page_title=page_title
|
248
254
|
)
|
249
255
|
)
|
250
256
|
|
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,14 +301,14 @@ 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
|
|
296
308
|
except Exception as e:
|
297
309
|
self.logger.error(
|
298
310
|
"Error fetching last_edited_time for database %s: %s",
|
299
|
-
self.
|
311
|
+
self.id,
|
300
312
|
str(e),
|
301
313
|
)
|
302
314
|
return None
|
@@ -345,14 +357,16 @@ class NotionDatabase(LoggingMixin):
|
|
345
357
|
current_body["start_cursor"] = start_cursor
|
346
358
|
|
347
359
|
result = await self.client.query_database(
|
348
|
-
database_id=self.
|
360
|
+
database_id=self.id, query_data=current_body
|
349
361
|
)
|
350
362
|
|
351
363
|
if not result or not result.results:
|
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
|
@@ -368,7 +382,7 @@ class NotionDatabase(LoggingMixin):
|
|
368
382
|
emoji_icon = cls._extract_emoji_icon(db_response)
|
369
383
|
|
370
384
|
instance = cls(
|
371
|
-
|
385
|
+
id=db_response.id,
|
372
386
|
title=title,
|
373
387
|
url=db_response.url,
|
374
388
|
emoji_icon=emoji_icon,
|
@@ -55,20 +55,16 @@ class NotionDatabaseProvider(LoggingMixin, metaclass=SingletonMetaClass):
|
|
55
55
|
database_name, token, min_similarity
|
56
56
|
)
|
57
57
|
|
58
|
-
id_cache_key = self._create_id_cache_key(database.
|
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.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.database_id})"
|
71
|
-
)
|
67
|
+
self.logger.debug(f"Cached database: {database.title} (ID: {database.id})")
|
72
68
|
|
73
69
|
return database
|
74
70
|
|
@@ -96,7 +92,7 @@ class NotionDatabaseProvider(LoggingMixin, metaclass=SingletonMetaClass):
|
|
96
92
|
name_keys_to_remove = [
|
97
93
|
cache_key
|
98
94
|
for cache_key, cached_db in self._database_cache.items()
|
99
|
-
if (cache_key.startswith("name:") and cached_db.
|
95
|
+
if (cache_key.startswith("name:") and cached_db.id == database_id)
|
100
96
|
]
|
101
97
|
|
102
98
|
for name_key in name_keys_to_remove:
|
@@ -173,7 +169,7 @@ class NotionDatabaseProvider(LoggingMixin, metaclass=SingletonMetaClass):
|
|
173
169
|
) -> None:
|
174
170
|
"""Cache a database by both ID and name (if provided)."""
|
175
171
|
# Always cache by ID
|
176
|
-
id_cache_key = self._create_id_cache_key(database.
|
172
|
+
id_cache_key = self._create_id_cache_key(database.id)
|
177
173
|
self._database_cache[id_cache_key] = database
|
178
174
|
|
179
175
|
if original_name:
|
@@ -199,7 +195,7 @@ class NotionDatabaseProvider(LoggingMixin, metaclass=SingletonMetaClass):
|
|
199
195
|
emoji_icon = self._extract_emoji_icon(db_response)
|
200
196
|
|
201
197
|
instance = NotionDatabase(
|
202
|
-
|
198
|
+
id=db_response.id,
|
203
199
|
title=title,
|
204
200
|
url=db_response.url,
|
205
201
|
emoji_icon=emoji_icon,
|
notionary/page/notion_page.py
CHANGED
@@ -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
|
-
database_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"
|
notionary/util/__init__.py
CHANGED
@@ -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
|
]
|