notionary 0.2.16__py3-none-any.whl → 0.2.17__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 +9 -5
  2. notionary/base_notion_client.py +18 -7
  3. notionary/blocks/__init__.py +2 -0
  4. notionary/blocks/document_element.py +194 -0
  5. notionary/database/__init__.py +4 -0
  6. notionary/database/database.py +481 -0
  7. notionary/database/{filter_builder.py → database_filter_builder.py} +27 -29
  8. notionary/database/{notion_database_provider.py → database_provider.py} +4 -4
  9. notionary/database/notion_database.py +45 -18
  10. notionary/file_upload/__init__.py +7 -0
  11. notionary/file_upload/client.py +254 -0
  12. notionary/file_upload/models.py +60 -0
  13. notionary/file_upload/notion_file_upload.py +387 -0
  14. notionary/page/notion_page.py +4 -3
  15. notionary/telemetry/views.py +15 -6
  16. notionary/user/__init__.py +11 -0
  17. notionary/user/base_notion_user.py +52 -0
  18. notionary/user/client.py +129 -0
  19. notionary/user/models.py +83 -0
  20. notionary/user/notion_bot_user.py +227 -0
  21. notionary/user/notion_user.py +256 -0
  22. notionary/user/notion_user_manager.py +173 -0
  23. notionary/user/notion_user_provider.py +1 -0
  24. notionary/util/__init__.py +3 -5
  25. notionary/util/{factory_decorator.py → factory_only.py} +9 -5
  26. notionary/util/fuzzy.py +74 -0
  27. notionary/util/logging_mixin.py +12 -12
  28. notionary/workspace.py +38 -2
  29. {notionary-0.2.16.dist-info → notionary-0.2.17.dist-info}/METADATA +2 -1
  30. {notionary-0.2.16.dist-info → notionary-0.2.17.dist-info}/RECORD +34 -20
  31. notionary/util/fuzzy_matcher.py +0 -82
  32. /notionary/database/{database_exceptions.py → exceptions.py} +0 -0
  33. /notionary/util/{singleton_decorator.py → singleton.py} +0 -0
  34. {notionary-0.2.16.dist-info → notionary-0.2.17.dist-info}/LICENSE +0 -0
  35. {notionary-0.2.16.dist-info → notionary-0.2.17.dist-info}/WHEEL +0 -0
@@ -0,0 +1,173 @@
1
+ from typing import Any, Dict, Optional, List
2
+ from notionary.user.client import NotionUserClient
3
+ from notionary.user.notion_user import NotionUser
4
+ from notionary.user.models import NotionUsersListResponse
5
+ from notionary.util import LoggingMixin
6
+
7
+
8
+ class NotionUserManager(LoggingMixin):
9
+ """
10
+ Manager for user operations within API limitations.
11
+
12
+ Note: The Notion API provides endpoints to list workspace users (excluding guests).
13
+ This manager provides utility functions for working with individual users and user lists.
14
+ """
15
+
16
+ def __init__(self, token: Optional[str] = None):
17
+ """Initialize the user manager."""
18
+ self.client = NotionUserClient(token=token)
19
+
20
+ async def get_current_bot_user(self) -> Optional[NotionUser]:
21
+ """
22
+ Get the current bot user from the API token.
23
+ """
24
+ return await NotionUser.current_bot_user(token=self.client.token)
25
+
26
+ async def get_user_by_id(self, user_id: str) -> Optional[NotionUser]:
27
+ """
28
+ Get a specific user by their ID.
29
+ """
30
+ return await NotionUser.from_user_id(user_id, token=self.client.token)
31
+
32
+ async def list_users(
33
+ self, page_size: int = 100, start_cursor: Optional[str] = None
34
+ ) -> Optional[NotionUsersListResponse]:
35
+ """
36
+ List users in the workspace (paginated).
37
+
38
+ Note: Guests are not included in the response.
39
+ """
40
+ try:
41
+ response = await self.client.list_users(page_size, start_cursor)
42
+ if response is None:
43
+ self.logger.error("Failed to list users")
44
+ return None
45
+
46
+ self.logger.info(
47
+ "Retrieved %d users (has_more: %s)",
48
+ len(response.results),
49
+ response.has_more,
50
+ )
51
+ return response
52
+
53
+ except Exception as e:
54
+ self.logger.error("Error listing users: %s", str(e))
55
+ return None
56
+
57
+ async def get_all_users(self) -> List[NotionUser]:
58
+ """
59
+ Get all users in the workspace as NotionUser objects.
60
+ Automatically handles pagination and converts responses to NotionUser instances.
61
+ """
62
+ try:
63
+ # Get raw user responses
64
+ user_responses = await self.client.get_all_users()
65
+
66
+ # Convert to NotionUser objects
67
+ notion_users = []
68
+ for user_response in user_responses:
69
+ try:
70
+ # Use the internal creation method to convert response to NotionUser
71
+ notion_user = NotionUser.from_notion_user_response(
72
+ user_response, self.client.token
73
+ )
74
+ notion_users.append(notion_user)
75
+ except Exception as e:
76
+ self.logger.warning(
77
+ "Failed to convert user %s to NotionUser: %s",
78
+ user_response.id,
79
+ str(e),
80
+ )
81
+ continue
82
+
83
+ self.logger.info(
84
+ "Successfully converted %d users to NotionUser objects",
85
+ len(notion_users),
86
+ )
87
+ return notion_users
88
+
89
+ except Exception as e:
90
+ self.logger.error("Error getting all users: %s", str(e))
91
+ return []
92
+
93
+ async def get_users_by_type(self, user_type: str = "person") -> List[NotionUser]:
94
+ """
95
+ Get all users of a specific type (person or bot).
96
+ """
97
+ try:
98
+ all_users = await self.get_all_users()
99
+ filtered_users = [user for user in all_users if user.user_type == user_type]
100
+
101
+ self.logger.info(
102
+ "Found %d users of type '%s' out of %d total users",
103
+ len(filtered_users),
104
+ user_type,
105
+ len(all_users),
106
+ )
107
+ return filtered_users
108
+
109
+ except Exception as e:
110
+ self.logger.error("Error filtering users by type: %s", str(e))
111
+ return []
112
+
113
+ # TODO: Type this
114
+ async def get_workspace_info(self) -> Optional[Dict[str, Any]]:
115
+ """
116
+ Get available workspace information from the bot user.
117
+ """
118
+ bot_user = await self.get_current_bot_user()
119
+ if bot_user is None:
120
+ self.logger.error("Failed to get bot user for workspace info")
121
+ return None
122
+
123
+ workspace_info = {
124
+ "workspace_name": bot_user.workspace_name,
125
+ "bot_user_id": bot_user.id,
126
+ "bot_user_name": bot_user.name,
127
+ "bot_user_type": bot_user.user_type,
128
+ }
129
+
130
+ # Add workspace limits if available
131
+ if bot_user.is_bot:
132
+ limits = await bot_user.get_workspace_limits()
133
+ if limits:
134
+ workspace_info["workspace_limits"] = limits
135
+
136
+ # Add user count statistics
137
+ try:
138
+ all_users = await self.get_all_users()
139
+ workspace_info["total_users"] = len(all_users)
140
+ workspace_info["person_users"] = len([u for u in all_users if u.is_person])
141
+ workspace_info["bot_users"] = len([u for u in all_users if u.is_bot])
142
+ except Exception as e:
143
+ self.logger.warning("Could not get user statistics: %s", str(e))
144
+
145
+ return workspace_info
146
+
147
+ async def find_users_by_name(self, name_pattern: str) -> List[NotionUser]:
148
+ """
149
+ Find users by name pattern (case-insensitive partial match).
150
+
151
+ Note: The API doesn't support server-side filtering, so this fetches all users
152
+ and filters client-side.
153
+ """
154
+ try:
155
+ all_users = await self.get_all_users()
156
+ pattern_lower = name_pattern.lower()
157
+
158
+ matching_users = [
159
+ user
160
+ for user in all_users
161
+ if user.name and pattern_lower in user.name.lower()
162
+ ]
163
+
164
+ self.logger.info(
165
+ "Found %d users matching pattern '%s'",
166
+ len(matching_users),
167
+ name_pattern,
168
+ )
169
+ return matching_users
170
+
171
+ except Exception as e:
172
+ self.logger.error("Error finding users by name: %s", str(e))
173
+ return []
@@ -0,0 +1 @@
1
+ # for caching shit
@@ -1,15 +1,13 @@
1
1
  from .logging_mixin import LoggingMixin
2
- from .singleton_decorator import singleton
2
+ from .singleton import singleton
3
3
  from .page_id_utils import format_uuid
4
- from .fuzzy_matcher import FuzzyMatcher
5
- from .factory_decorator import factory_only
4
+ from .factory_only import factory_only
6
5
  from .singleton_metaclass import SingletonMetaClass
7
6
 
8
7
  __all__ = [
9
8
  "LoggingMixin",
10
- "singleton_decorator",
9
+ "singleton",
11
10
  "format_uuid",
12
- "FuzzyMatcher",
13
11
  "factory_only",
14
12
  "singleton",
15
13
  "SingletonMetaClass",
@@ -1,10 +1,11 @@
1
1
  import functools
2
2
  import inspect
3
+ import warnings
3
4
 
4
5
 
5
6
  def factory_only(*allowed_factories):
6
7
  """
7
- Decorator that ensures __init__ is only called from allowed factory methods.
8
+ Decorator that warns when __init__ is not called from allowed factory methods.
8
9
 
9
10
  Args:
10
11
  *allowed_factories: Names of allowed factory methods (e.g. 'from_database_id')
@@ -21,10 +22,13 @@ def factory_only(*allowed_factories):
21
22
  caller_name = caller_frame.f_code.co_name
22
23
  if caller_name in allowed_factories or caller_name.startswith("_"):
23
24
  return init_method(self, *args, **kwargs)
24
- else:
25
- raise RuntimeError(
26
- f"Direct instantiation not allowed! Use one of: {', '.join(allowed_factories)}"
27
- )
25
+
26
+ warnings.warn(
27
+ f"Direct instantiation not recommended! Consider using one of: {', '.join(allowed_factories)}",
28
+ UserWarning,
29
+ stacklevel=3,
30
+ )
31
+ return init_method(self, *args, **kwargs)
28
32
  finally:
29
33
  del frame
30
34
 
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ import difflib
3
+ from typing import List, Any, TypeVar, Callable, Optional
4
+ from dataclasses import dataclass
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ @dataclass
10
+ class MatchResult:
11
+ """Result of a fuzzy match operation."""
12
+
13
+ item: Any
14
+ similarity: float
15
+ matched_text: str
16
+
17
+
18
+ def calculate_similarity(query: str, target: str) -> float:
19
+ """Calculate similarity between two strings using difflib."""
20
+ return difflib.SequenceMatcher(
21
+ None, query.lower().strip(), target.lower().strip()
22
+ ).ratio()
23
+
24
+
25
+ def find_best_matches(
26
+ query: str,
27
+ items: List[T],
28
+ text_extractor: Callable[[T], str],
29
+ min_similarity: float = 0.0,
30
+ limit: Optional[int] = None,
31
+ ) -> List[MatchResult[T]]:
32
+ """
33
+ Find best fuzzy matches from a list of items.
34
+
35
+ Args:
36
+ query: The search query
37
+ items: List of items to search through
38
+ text_extractor: Function to extract text from each item
39
+ min_similarity: Minimum similarity threshold (0.0 to 1.0)
40
+ limit: Maximum number of results to return
41
+
42
+ Returns:
43
+ List of MatchResult objects sorted by similarity (highest first)
44
+ """
45
+ results = []
46
+
47
+ for item in items:
48
+ text = text_extractor(item)
49
+ similarity = calculate_similarity(query, text)
50
+
51
+ if similarity >= min_similarity:
52
+ results.append(
53
+ MatchResult(item=item, similarity=similarity, matched_text=text)
54
+ )
55
+
56
+ # Sort by similarity (highest first)
57
+ results.sort(key=lambda x: x.similarity, reverse=True)
58
+
59
+ # Apply limit if specified
60
+ if limit:
61
+ results = results[:limit]
62
+
63
+ return results
64
+
65
+
66
+ def find_best_match(
67
+ query: str,
68
+ items: List[T],
69
+ text_extractor: Callable[[T], str],
70
+ min_similarity: float = 0.0,
71
+ ) -> Optional[MatchResult[T]]:
72
+ """Find the single best fuzzy match."""
73
+ matches = find_best_matches(query, items, text_extractor, min_similarity, limit=1)
74
+ return matches[0] if matches else None
@@ -1,14 +1,24 @@
1
1
  import logging
2
- import inspect
3
2
  from typing import Optional, ClassVar
4
3
 
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
5
9
 
6
10
  def setup_logging():
11
+ """
12
+ Sets up logging configuration for the application.
13
+ """
14
+ log_level = os.getenv("LOG_LEVEL", "WARNING").upper()
7
15
  logging.basicConfig(
8
- level=logging.INFO,
16
+ level=getattr(logging, log_level),
9
17
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
10
18
  )
11
19
 
20
+ logging.getLogger("httpx").setLevel(logging.WARNING)
21
+
12
22
 
13
23
  setup_logging()
14
24
 
@@ -32,16 +42,6 @@ class LoggingMixin:
32
42
  self._logger = logging.getLogger(self.__class__.__name__)
33
43
  return self._logger
34
44
 
35
- @staticmethod
36
- def static_logger() -> logging.Logger:
37
- """Static logger - for static methods"""
38
- stack = inspect.stack()
39
- for frame_info in stack[1:]:
40
- class_name = LoggingMixin._get_class_name_from_frame(frame_info.frame)
41
- if class_name:
42
- return logging.getLogger(class_name)
43
- return logging.getLogger("UnknownStaticContext")
44
-
45
45
  @staticmethod
46
46
  def _get_class_name_from_frame(frame) -> Optional[str]:
47
47
  local_vars = frame.f_locals
notionary/workspace.py CHANGED
@@ -3,20 +3,25 @@ from typing import Optional, List
3
3
  from notionary import NotionPage, NotionDatabase
4
4
  from notionary.database.client import NotionDatabaseClient
5
5
  from notionary.page.client import NotionPageClient
6
+ from notionary.user import NotionUser, NotionUserManager
6
7
  from notionary.util import LoggingMixin
7
8
 
8
9
 
9
10
  class NotionWorkspace(LoggingMixin):
10
11
  """
11
- Represents a Notion workspace, providing methods to interact with databases and pages.
12
+ Represents a Notion workspace, providing methods to interact with databases, pages, and limited user operations.
13
+
14
+ Note: Due to Notion API limitations, bulk user operations (listing all users) are not supported.
15
+ Only individual user queries and bot user information are available.
12
16
  """
13
17
 
14
18
  def __init__(self, token: Optional[str] = None):
15
19
  """
16
- Initialize the workspace with a Notion database_client.
20
+ Initialize the workspace with Notion clients.
17
21
  """
18
22
  self.database_client = NotionDatabaseClient(token=token)
19
23
  self.page_client = NotionPageClient(token=token)
24
+ self.user_manager = NotionUserManager(token=token)
20
25
 
21
26
  async def search_pages(self, query: str, limit=100) -> List[NotionPage]:
22
27
  """
@@ -66,4 +71,35 @@ class NotionWorkspace(LoggingMixin):
66
71
  for database in database_results.results
67
72
  ]
68
73
 
74
+ # User-related methods (limited due to API constraints)
75
+ async def get_current_bot_user(self) -> Optional[NotionUser]:
76
+ """
77
+ Get the current bot user from the API token.
78
+
79
+ Returns:
80
+ Optional[NotionUser]: Current bot user or None if failed
81
+ """
82
+ return await self.user_manager.get_current_bot_user()
83
+
84
+ async def get_user_by_id(self, user_id: str) -> Optional[NotionUser]:
85
+ """
86
+ Get a specific user by their ID.
87
+
88
+ Args:
89
+ user_id: The ID of the user to retrieve
90
+
91
+ Returns:
92
+ Optional[NotionUser]: The user or None if not found/failed
93
+ """
94
+ return await self.user_manager.get_user_by_id(user_id)
95
+
96
+ async def get_workspace_info(self) -> Optional[dict]:
97
+ """
98
+ Get available workspace information including bot details.
99
+
100
+ Returns:
101
+ Optional[dict]: Workspace information or None if failed to get bot user
102
+ """
103
+ return await self.user_manager.get_workspace_info()
104
+
69
105
  # TODO: Create database would be nice here
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: notionary
3
- Version: 0.2.16
3
+ Version: 0.2.17
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
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
16
17
  Requires-Dist: httpx (>=0.28.0)
17
18
  Requires-Dist: posthog (>=6.3.1,<7.0.0)
18
19
  Requires-Dist: pydantic (>=2.11.4)
@@ -1,6 +1,6 @@
1
- notionary/__init__.py,sha256=4eO6Jx57VRR_Ejo9w7IJeET8SZOvxFl_1lOB39o39No,250
2
- notionary/base_notion_client.py,sha256=hJnN8CZe7CUunMBljGdKKN44xMlJQAIAhQyT-3WEIK8,6722
3
- notionary/blocks/__init__.py,sha256=MFBxK3zZ28tV_u8XT20Q6HY39KENCfJDfDflLTYVt4E,2019
1
+ notionary/__init__.py,sha256=oVZiZglID0yNOWB6VjIpN9HMpsROBDRraWJjUp43qvQ,511
2
+ notionary/base_notion_client.py,sha256=WIAIKZeGCeAwtHWnL4Kfcmzv2TBlxvz7cfGF26i89y0,7067
3
+ notionary/blocks/__init__.py,sha256=-Za7ipQtuHtYplx9yYuA4Smt0c1JxU0ymOWw1EFEfMs,2090
4
4
  notionary/blocks/audio_element.py,sha256=rQbWz8akbobci8CFvnFuuHoDNJCG7mcuSXdB8hHjqLU,5355
5
5
  notionary/blocks/bookmark_element.py,sha256=gW6uKCkuWFpHEzq-g1CbvKvma6hyTMUH2XMczI0U-5M,8080
6
6
  notionary/blocks/bulleted_list_element.py,sha256=Uv_ohhF0MTwQ29w_RUX91NFuz57Dtr4vQpV8seRAgy0,2599
@@ -8,6 +8,7 @@ notionary/blocks/callout_element.py,sha256=Cya-1HIRBiCiyMgQq6PqXU4_iGj2O3qAPirht
8
8
  notionary/blocks/code_block_element.py,sha256=w0AN5m1qEFEEMDZ5dicCUhh4RwQpjByzDW3PuHgvgt0,7466
9
9
  notionary/blocks/column_element.py,sha256=qzbrTcWWOhGts2fAWHTwQUW8Ca6yoQEMZol9ZxUDCCI,12669
10
10
  notionary/blocks/divider_element.py,sha256=eCX2OupttnjGUaIaF59RhULKqh8R6d8KPnpctMMaXJs,2267
11
+ notionary/blocks/document_element.py,sha256=VgICkBrSLKzQ_kuyzMiaJg__ccCiM6x1hqGj5lfwANQ,7035
11
12
  notionary/blocks/embed_element.py,sha256=MFHh3ZFNntvaJ1NiEs0bzpbmJTRm0Axqdtf5oputbi0,4516
12
13
  notionary/blocks/heading_element.py,sha256=aALMpclbPTvKfJOICJdgP0y-y7w5jhv7rUQl04TQpeg,3051
13
14
  notionary/blocks/image_element.py,sha256=SEZ31_uDBRy6_lpn8E_GMX5uzI7-c-pJB9idUGZiTrE,4695
@@ -27,15 +28,20 @@ notionary/blocks/todo_element.py,sha256=6ndhgGJNiy7eb-Ll--Va7zEqQySxFAFYpzY4PWJb
27
28
  notionary/blocks/toggle_element.py,sha256=2gofKL4ndVkRxkuH-iYVx0YFUc649gpQQbZtwh2BpY8,11017
28
29
  notionary/blocks/toggleable_heading_element.py,sha256=fkXvKtgCg6PuHqrHq7LupmqzpasJ1IyVf2RBLYTiVIo,9893
29
30
  notionary/blocks/video_element.py,sha256=C19XxFRyAUEbhhC9xvhAAGN8YBYP6ON1vm_x7b_gUrY,5664
30
- notionary/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ notionary/database/__init__.py,sha256=4tdML0fBzkOCpiWT6q-L--5NELFLbTPD0IUA_E8yZno,155
31
32
  notionary/database/client.py,sha256=ZcfydeYlpgGJt6wV1ib33KeXUiL-cGNJ1qraQZ4RVRc,4775
32
- notionary/database/database_exceptions.py,sha256=jwFdxoIQHLO3mO3p5t890--1FjbTX60fNyqBAe-sszo,452
33
+ notionary/database/database.py,sha256=7KBe6DWbtsakPvjUO45eCphD2sq0ZKuYHY3kXJsk4P8,16582
34
+ notionary/database/database_filter_builder.py,sha256=PCnq3M9REt-NHRgBIfSrTCIPykUqBs5RqZK13WOAHKA,5950
35
+ notionary/database/database_provider.py,sha256=MveBSMkYG8nLUzKTS7KjbFVH8H-vLdH0t6yGArs-vPQ,8976
36
+ notionary/database/exceptions.py,sha256=jwFdxoIQHLO3mO3p5t890--1FjbTX60fNyqBAe-sszo,452
33
37
  notionary/database/factory.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- notionary/database/filter_builder.py,sha256=4EJnWUF73l5oi-HnvMu-mI1OncLzEs2o2mr_xG75quk,6315
35
38
  notionary/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
36
- notionary/database/notion_database.py,sha256=jA3_x-pMJdcI6-ZDiSrx43ywaFaw0MLRW8wYb7DOlvQ,15755
37
- notionary/database/notion_database_provider.py,sha256=GVfS8fgf5RhX15y8gpvRjBkQbv--8WFgKBkI_Z5LRaU,9009
39
+ notionary/database/notion_database.py,sha256=wSqPfVtOnDL-aKRrE9BSMP1cpHe0_8RYyfCMxrlJSNo,16746
38
40
  notionary/elements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ notionary/file_upload/__init__.py,sha256=7TNyiIgLMD_IGRXTwRiAmStokF3rLoG4zXPwNb9KQqk,168
42
+ notionary/file_upload/client.py,sha256=qlxgu7_Ia0wbBflInoGki4mR8Po_RZgfLjO2wa279jY,8581
43
+ notionary/file_upload/models.py,sha256=0mYtuGkZ_eh_YmX0uxqye5vg3wWgqWuwOupAmLJXMUY,1625
44
+ notionary/file_upload/notion_file_upload.py,sha256=DZHRyKtGcLubgNE6yg5Nibs7mpWxp18YFBGJBBxSjsk,13163
39
45
  notionary/models/notion_block_response.py,sha256=gzL4C6K9QPcaMS6NbAZaRceSEnMbNwYBVVzxysza5VU,6002
40
46
  notionary/models/notion_database_response.py,sha256=3kvADIP1dSxgITSK4n8Ex3QpF8n_Lxnu_IXbPVGcq4o,7648
41
47
  notionary/models/notion_page_response.py,sha256=7ZwDYhlyK-avix_joQpGuNQZopjlQFI8jS3nvNNumoc,1544
@@ -48,7 +54,7 @@ notionary/page/content/page_content_writer.py,sha256=VVvK-Z8NvyIhi7Crcm9mZQuuD_L
48
54
  notionary/page/formatting/markdown_to_notion_converter.py,sha256=9RyGON8VrJv6XifdQdOt5zKgKT3irc974zcbGDBhmLY,17328
49
55
  notionary/page/formatting/spacer_rules.py,sha256=j2RHvdXT3HxXPVBEuCtulyy9cPxsEcOmj71pJqV-D3M,15677
50
56
  notionary/page/markdown_syntax_prompt_generator.py,sha256=uHCPNV9aQi3GzLVimyUKwza29hfxu6DTMVIa_QevJbk,4987
51
- notionary/page/notion_page.py,sha256=PAwixEuzn5mnkIU4AtOKHiaaG98fmVqFhPe4Mjnb9bA,19110
57
+ notionary/page/notion_page.py,sha256=4KXPtDORuk7-8bJ5NV_x9RplLTgNHl72ngECc_M32xA,19126
52
58
  notionary/page/notion_to_markdown_converter.py,sha256=_MJWWwsBvgZ3a8tLZ23ZCIA_G9Qfvt2JG1FqVTlRxHs,6308
53
59
  notionary/page/properites/property_value_extractor.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
60
  notionary/page/property_formatter.py,sha256=_978ViH83gfcr-XtDscWTfyBI2srGW2hzC-gzgp5NR8,3788
@@ -56,16 +62,24 @@ notionary/page/search_filter_builder.py,sha256=wZpW_KHmPXql3sNIyQd9EzZ2-ERy2i0vY
56
62
  notionary/page/utils.py,sha256=2nfBrWeczBdPH13R3q8dKP4OY4MwEdfKbcs2UJ9kg1o,2041
57
63
  notionary/telemetry/__init__.py,sha256=Y7KyXeN4PiA6GtzV3NnwoH4hJnPwdjikWP22ckPYuHM,511
58
64
  notionary/telemetry/service.py,sha256=DD7RbkSN0HWRK2YpOJTgFD7PeXGhSe9KrLkhiVIaC7Y,4763
59
- notionary/telemetry/views.py,sha256=BXVa25h0A4leGaz5U9F-T5ebShkojD-DvYElWkrP6U4,1762
60
- notionary/util/__init__.py,sha256=JAOxIchioEx4h_KcRs8mqgBjPveoNmzqkNzdQZnokNk,438
61
- notionary/util/factory_decorator.py,sha256=3SD63EPxXMmKQ8iF7sF88xUFMG8dy14L2DJZ7XdcYm4,1110
62
- notionary/util/fuzzy_matcher.py,sha256=RYR86hMTp8lrWl3PeOa3RpDpzh04HJ30qrIlrq6_qDo,2442
63
- notionary/util/logging_mixin.py,sha256=d5sRSmUtgQeuckdNBkO025IXPGe4oOb-7ueVAIP8amU,1846
65
+ notionary/telemetry/views.py,sha256=FgFZGYaxP7pRYx-9wg18skMh_MJAwf4W3rCfe9JOZe4,1796
66
+ notionary/user/__init__.py,sha256=D8r_WtQimdT-l3P1wd8O9Iki5JXF7jg2KV_hOgzWraw,281
67
+ notionary/user/base_notion_user.py,sha256=QB1701CfQTeu1ZNWOHYnGvTSHZuiritr6xYNMqyVg_U,1500
68
+ notionary/user/client.py,sha256=v8-lpyKzY_gSWPdWkBEjqxEPSg1WCy9AYSAJLXRrfKA,4563
69
+ notionary/user/models.py,sha256=3WPUysO96hMZUZYUWoCH5cdqNvnWgdiw5Qd7QKOv2UY,2063
70
+ notionary/user/notion_bot_user.py,sha256=cwrwmfst5RbAgppIjoFMJjExms1epfRM9VBd-S-3y5Q,7784
71
+ notionary/user/notion_user.py,sha256=l0GOMakk_xzsUUt7SsDJ5j-3dbtX64cUyyQPxn0r0Zc,8512
72
+ notionary/user/notion_user_manager.py,sha256=JQddAvbKJiLCLCn0mfxmvoi5uLW95vHhMWEYZD_F9zk,6339
73
+ notionary/user/notion_user_provider.py,sha256=7ImQ7T76g88W4YyHGOoO0aUJQfdGlhbh1fMT9PzR1mU,19
74
+ notionary/util/__init__.py,sha256=Bu2yGFd3xPasBrMRMk-6DcmMXfkdXn-KfFRETApBcP8,351
75
+ notionary/util/factory_only.py,sha256=q-ZXUEvQ8lsuD1JQwx-ai6e9Lhie65waRE1BcRhCR_4,1235
76
+ notionary/util/fuzzy.py,sha256=lTlWuYEs0lhLa7QIRre3GVJ8-biIV_NmcJnMzBYFF5U,2061
77
+ notionary/util/logging_mixin.py,sha256=t08dx3nhSJq2-6_N8G39aydPEkQ76D4MlRfymxCsCVM,1690
64
78
  notionary/util/page_id_utils.py,sha256=AA00kRO-g3Cc50tf_XW_tb5RBuPKLuBxRa0D8LYhLXg,736
65
- notionary/util/singleton_decorator.py,sha256=CKAvykndwPRZsA3n3MAY_XdCR59MBjjKP0vtm2BcvF0,428
79
+ notionary/util/singleton.py,sha256=CKAvykndwPRZsA3n3MAY_XdCR59MBjjKP0vtm2BcvF0,428
66
80
  notionary/util/singleton_metaclass.py,sha256=uNeHiqS6TwhljvG1RE4NflIp2HyMuMmrCg2xI-vxmHE,809
67
- notionary/workspace.py,sha256=kW9fbVUSECivlvABBwnks2nALfk09V6g6Oc2Eq_pK5U,2511
68
- notionary-0.2.16.dist-info/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
69
- notionary-0.2.16.dist-info/METADATA,sha256=qy_xKJWjzGlY0zZcJ4T2hZtFgjMmgZhRSEqaQc8Hlqo,6824
70
- notionary-0.2.16.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
71
- notionary-0.2.16.dist-info/RECORD,,
81
+ notionary/workspace.py,sha256=W3YybEUI7Kwp1sa7DRI-kc7ViMBLRs1kIkum2G8bqqM,3878
82
+ notionary-0.2.17.dist-info/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
83
+ notionary-0.2.17.dist-info/METADATA,sha256=b9HAyv9WytOyw5-OnObrvTs1FOfAuho1-gCCzeQ9frk,6867
84
+ notionary-0.2.17.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
85
+ notionary-0.2.17.dist-info/RECORD,,
@@ -1,82 +0,0 @@
1
- from __future__ import annotations
2
- import difflib
3
- from typing import List, Any, TypeVar, Callable, Optional
4
- from dataclasses import dataclass
5
-
6
- T = TypeVar("T")
7
-
8
-
9
- @dataclass
10
- class MatchResult:
11
- """Result of a fuzzy match operation."""
12
-
13
- item: Any
14
- similarity: float
15
- matched_text: str
16
-
17
-
18
- class FuzzyMatcher:
19
- """Utility class for fuzzy string matching operations."""
20
-
21
- @staticmethod
22
- def calculate_similarity(query: str, target: str) -> float:
23
- """Calculate similarity between two strings using difflib."""
24
- return difflib.SequenceMatcher(
25
- None, query.lower().strip(), target.lower().strip()
26
- ).ratio()
27
-
28
- @classmethod
29
- def find_best_matches(
30
- cls,
31
- query: str,
32
- items: List[T],
33
- text_extractor: Callable[[T], str],
34
- min_similarity: float = 0.0,
35
- limit: Optional[int] = None,
36
- ) -> List[MatchResult[T]]:
37
- """
38
- Find best fuzzy matches from a list of items.
39
-
40
- Args:
41
- query: The search query
42
- items: List of items to search through
43
- text_extractor: Function to extract text from each item
44
- min_similarity: Minimum similarity threshold (0.0 to 1.0)
45
- limit: Maximum number of results to return
46
-
47
- Returns:
48
- List of MatchResult objects sorted by similarity (highest first)
49
- """
50
- results = []
51
-
52
- for item in items:
53
- text = text_extractor(item)
54
- similarity = cls.calculate_similarity(query, text)
55
-
56
- if similarity >= min_similarity:
57
- results.append(
58
- MatchResult(item=item, similarity=similarity, matched_text=text)
59
- )
60
-
61
- # Sort by similarity (highest first)
62
- results.sort(key=lambda x: x.similarity, reverse=True)
63
-
64
- # Apply limit if specified
65
- if limit:
66
- results = results[:limit]
67
-
68
- return results
69
-
70
- @classmethod
71
- def find_best_match(
72
- cls,
73
- query: str,
74
- items: List[T],
75
- text_extractor: Callable[[T], str],
76
- min_similarity: float = 0.0,
77
- ) -> Optional[MatchResult[T]]:
78
- """Find the single best fuzzy match."""
79
- matches = cls.find_best_matches(
80
- query, items, text_extractor, min_similarity, limit=1
81
- )
82
- return matches[0] if matches else None
File without changes