notionary 0.2.13__py3-none-any.whl → 0.2.14__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/__init__.py +3 -16
- notionary/{notion_client.py → base_notion_client.py} +92 -98
- notionary/blocks/__init__.py +61 -0
- notionary/{elements → blocks}/audio_element.py +6 -3
- notionary/{elements → blocks}/bookmark_element.py +3 -5
- notionary/{elements → blocks}/bulleted_list_element.py +5 -6
- notionary/{elements → blocks}/callout_element.py +4 -6
- notionary/{elements → blocks}/code_block_element.py +4 -5
- notionary/{elements → blocks}/column_element.py +3 -5
- notionary/{elements → blocks}/divider_element.py +3 -5
- notionary/{elements → blocks}/embed_element.py +4 -5
- notionary/{elements → blocks}/heading_element.py +4 -7
- notionary/{elements → blocks}/image_element.py +4 -5
- notionary/{elements → blocks}/mention_element.py +3 -6
- notionary/blocks/notion_block_client.py +26 -0
- notionary/{elements → blocks}/notion_block_element.py +2 -3
- notionary/{elements → blocks}/numbered_list_element.py +4 -6
- notionary/{elements → blocks}/paragraph_element.py +4 -6
- notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
- notionary/blocks/prompts/element_prompt_content.py +41 -0
- notionary/{elements → blocks}/qoute_element.py +4 -5
- notionary/{elements → blocks}/registry/block_registry.py +4 -26
- notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
- notionary/{elements → blocks}/table_element.py +5 -6
- notionary/{elements → blocks}/text_inline_formatter.py +1 -4
- notionary/{elements → blocks}/todo_element.py +5 -6
- notionary/{elements → blocks}/toggle_element.py +3 -5
- notionary/{elements → blocks}/toggleable_heading_element.py +4 -6
- notionary/{elements → blocks}/video_element.py +4 -5
- notionary/cli/main.py +157 -128
- notionary/cli/onboarding.py +10 -9
- notionary/database/__init__.py +0 -0
- notionary/database/client.py +132 -0
- notionary/database/database_exceptions.py +13 -0
- notionary/database/factory.py +0 -0
- notionary/database/filter_builder.py +175 -0
- notionary/database/notion_database.py +339 -126
- notionary/database/notion_database_provider.py +230 -0
- notionary/elements/__init__.py +0 -0
- notionary/models/notion_database_response.py +294 -13
- notionary/models/notion_page_response.py +9 -31
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/client.py +110 -0
- notionary/page/content/page_content_retriever.py +5 -20
- notionary/page/content/page_content_writer.py +3 -4
- notionary/page/formatting/markdown_to_notion_converter.py +1 -3
- notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
- notionary/page/notion_page.py +354 -317
- notionary/page/notion_to_markdown_converter.py +1 -4
- notionary/page/properites/property_value_extractor.py +0 -64
- notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
- notionary/page/search_filter_builder.py +131 -0
- notionary/page/utils.py +60 -0
- notionary/util/__init__.py +12 -3
- notionary/util/factory_decorator.py +33 -0
- notionary/util/fuzzy_matcher.py +82 -0
- notionary/util/page_id_utils.py +0 -21
- notionary/util/singleton_metaclass.py +22 -0
- notionary/workspace.py +69 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
- notionary-0.2.14.dist-info/RECORD +72 -0
- notionary/database/database_discovery.py +0 -142
- notionary/database/notion_database_factory.py +0 -190
- notionary/exceptions/database_exceptions.py +0 -76
- notionary/exceptions/page_creation_exception.py +0 -9
- notionary/page/metadata/metadata_editor.py +0 -150
- notionary/page/metadata/notion_icon_manager.py +0 -77
- notionary/page/metadata/notion_page_cover_manager.py +0 -56
- notionary/page/notion_page_factory.py +0 -328
- notionary/page/properites/database_property_service.py +0 -302
- notionary/page/properites/page_property_manager.py +0 -152
- notionary/page/relations/notion_page_relation_manager.py +0 -350
- notionary/page/relations/notion_page_title_resolver.py +0 -104
- notionary/page/relations/page_database_relation.py +0 -68
- notionary/util/warn_direct_constructor_usage.py +0 -54
- notionary-0.2.13.dist-info/RECORD +0 -67
- /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,6 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional
|
2
2
|
|
3
|
-
from notionary.
|
4
|
-
from notionary.elements.registry.block_registry_builder import (
|
5
|
-
BlockRegistryBuilder,
|
6
|
-
)
|
3
|
+
from notionary.blocks import BlockRegistry, BlockRegistryBuilder
|
7
4
|
|
8
5
|
|
9
6
|
class NotionToMarkdownConverter:
|
@@ -1,64 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
from typing import Any, Awaitable, Callable
|
3
|
-
|
4
|
-
from notionary.util import LoggingMixin
|
5
|
-
|
6
|
-
|
7
|
-
class PropertyValueExtractor(LoggingMixin):
|
8
|
-
|
9
|
-
async def extract(
|
10
|
-
self,
|
11
|
-
property_name: str,
|
12
|
-
prop_data: dict,
|
13
|
-
relation_resolver: Callable[[str], Awaitable[Any]],
|
14
|
-
) -> Any:
|
15
|
-
prop_type = prop_data.get("type")
|
16
|
-
if not prop_type:
|
17
|
-
return None
|
18
|
-
|
19
|
-
handlers: dict[str, Callable[[], Awaitable[Any] | Any]] = {
|
20
|
-
"title": lambda: "".join(
|
21
|
-
t.get("plain_text", "") for t in prop_data.get("title", [])
|
22
|
-
),
|
23
|
-
"rich_text": lambda: "".join(
|
24
|
-
t.get("plain_text", "") for t in prop_data.get("rich_text", [])
|
25
|
-
),
|
26
|
-
"number": lambda: prop_data.get("number"),
|
27
|
-
"select": lambda: (
|
28
|
-
prop_data.get("select", {}).get("name")
|
29
|
-
if prop_data.get("select")
|
30
|
-
else None
|
31
|
-
),
|
32
|
-
"multi_select": lambda: [
|
33
|
-
o.get("name") for o in prop_data.get("multi_select", [])
|
34
|
-
],
|
35
|
-
"status": lambda: (
|
36
|
-
prop_data.get("status", {}).get("name")
|
37
|
-
if prop_data.get("status")
|
38
|
-
else None
|
39
|
-
),
|
40
|
-
"date": lambda: prop_data.get("date"),
|
41
|
-
"checkbox": lambda: prop_data.get("checkbox"),
|
42
|
-
"url": lambda: prop_data.get("url"),
|
43
|
-
"email": lambda: prop_data.get("email"),
|
44
|
-
"phone_number": lambda: prop_data.get("phone_number"),
|
45
|
-
"relation": lambda: relation_resolver(property_name),
|
46
|
-
"people": lambda: [p.get("id") for p in prop_data.get("people", [])],
|
47
|
-
"files": lambda: [
|
48
|
-
(
|
49
|
-
f.get("external", {}).get("url")
|
50
|
-
if f.get("type") == "external"
|
51
|
-
else f.get("name")
|
52
|
-
)
|
53
|
-
for f in prop_data.get("files", [])
|
54
|
-
],
|
55
|
-
}
|
56
|
-
|
57
|
-
handler = handlers.get(prop_type)
|
58
|
-
if handler is None:
|
59
|
-
if self.logger:
|
60
|
-
self.logger.warning(f"Unsupported property type: {prop_type}")
|
61
|
-
return None
|
62
|
-
|
63
|
-
result = handler()
|
64
|
-
return await result if asyncio.iscoroutine(result) else result
|
@@ -3,6 +3,7 @@ from typing import Any, Dict, Optional
|
|
3
3
|
from notionary.util import LoggingMixin
|
4
4
|
|
5
5
|
|
6
|
+
# TODO: mit dem Utils.py hier im order zusammenfassen
|
6
7
|
class NotionPropertyFormatter(LoggingMixin):
|
7
8
|
"""Class for formatting Notion properties based on their type."""
|
8
9
|
|
@@ -76,7 +77,9 @@ class NotionPropertyFormatter(LoggingMixin):
|
|
76
77
|
return {"relation": [{"id": item} for item in value]}
|
77
78
|
return {"relation": [{"id": str(value)}]}
|
78
79
|
|
79
|
-
def format_value(
|
80
|
+
def format_value(
|
81
|
+
self, property_name, property_type: str, value: Any
|
82
|
+
) -> Optional[Dict[str, Any]]:
|
80
83
|
"""
|
81
84
|
Formats a value according to the given Notion property type.
|
82
85
|
|
@@ -89,8 +92,8 @@ class NotionPropertyFormatter(LoggingMixin):
|
|
89
92
|
"""
|
90
93
|
formatter = self._formatters.get(property_type)
|
91
94
|
if not formatter:
|
92
|
-
|
93
|
-
self.logger.warning("Unknown property type: %s", property_type)
|
95
|
+
self.logger.warning("Unknown property type: %s", property_type)
|
94
96
|
return None
|
95
97
|
|
96
|
-
|
98
|
+
formatted_property = formatter(value)
|
99
|
+
return {"properties": {property_name: formatted_property}}
|
@@ -0,0 +1,131 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Any, Dict, Optional, Literal
|
3
|
+
from dataclasses import dataclass
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class SearchConfig:
|
8
|
+
"""Konfiguration für Notion Search API Filter."""
|
9
|
+
|
10
|
+
query: Optional[str] = None
|
11
|
+
object_type: Optional[Literal["page", "database"]] = None
|
12
|
+
sort_direction: Literal["ascending", "descending"] = "descending"
|
13
|
+
sort_timestamp: Literal["last_edited_time", "created_time"] = "last_edited_time"
|
14
|
+
page_size: int = 100
|
15
|
+
start_cursor: Optional[str] = None
|
16
|
+
|
17
|
+
def to_search_dict(self) -> Dict[str, Any]:
|
18
|
+
"""Konvertiert zu einem Notion Search API Dictionary."""
|
19
|
+
search_dict = {}
|
20
|
+
|
21
|
+
if self.query:
|
22
|
+
search_dict["query"] = self.query
|
23
|
+
|
24
|
+
if self.object_type:
|
25
|
+
search_dict["filter"] = {"property": "object", "value": self.object_type}
|
26
|
+
|
27
|
+
search_dict["sort"] = {
|
28
|
+
"direction": self.sort_direction,
|
29
|
+
"timestamp": self.sort_timestamp,
|
30
|
+
}
|
31
|
+
|
32
|
+
search_dict["page_size"] = min(self.page_size, 100)
|
33
|
+
|
34
|
+
if self.start_cursor:
|
35
|
+
search_dict["start_cursor"] = self.start_cursor
|
36
|
+
|
37
|
+
return search_dict
|
38
|
+
|
39
|
+
|
40
|
+
class SearchFilterBuilder:
|
41
|
+
"""
|
42
|
+
Builder class for creating Notion Search API filters with comprehensive options.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, config: SearchConfig = None):
|
46
|
+
self.config = config or SearchConfig()
|
47
|
+
|
48
|
+
def with_query(self, query: str) -> SearchFilterBuilder:
|
49
|
+
"""Set the search query string."""
|
50
|
+
self.config.query = query
|
51
|
+
return self
|
52
|
+
|
53
|
+
def with_pages_only(self) -> SearchFilterBuilder:
|
54
|
+
"""Filter to only return pages."""
|
55
|
+
self.config.object_type = "page"
|
56
|
+
return self
|
57
|
+
|
58
|
+
def with_databases_only(self) -> SearchFilterBuilder:
|
59
|
+
"""Filter to only return databases."""
|
60
|
+
self.config.object_type = "database"
|
61
|
+
return self
|
62
|
+
|
63
|
+
def with_sort_direction(
|
64
|
+
self, direction: Literal["ascending", "descending"]
|
65
|
+
) -> SearchFilterBuilder:
|
66
|
+
"""Set sort direction (ascending or descending)."""
|
67
|
+
self.config.sort_direction = direction
|
68
|
+
return self
|
69
|
+
|
70
|
+
def with_sort_ascending(self) -> SearchFilterBuilder:
|
71
|
+
"""Sort results in ascending order."""
|
72
|
+
return self.with_sort_direction("ascending")
|
73
|
+
|
74
|
+
def with_sort_descending(self) -> SearchFilterBuilder:
|
75
|
+
"""Sort results in descending order."""
|
76
|
+
return self.with_sort_direction("descending")
|
77
|
+
|
78
|
+
def with_sort_timestamp(
|
79
|
+
self, timestamp: Literal["last_edited_time", "created_time"]
|
80
|
+
) -> SearchFilterBuilder:
|
81
|
+
"""Set the timestamp field to sort by."""
|
82
|
+
self.config.sort_timestamp = timestamp
|
83
|
+
return self
|
84
|
+
|
85
|
+
def with_sort_by_created_time(self) -> SearchFilterBuilder:
|
86
|
+
"""Sort by creation time."""
|
87
|
+
return self.with_sort_timestamp("created_time")
|
88
|
+
|
89
|
+
def with_sort_by_last_edited(self) -> SearchFilterBuilder:
|
90
|
+
"""Sort by last edited time."""
|
91
|
+
return self.with_sort_timestamp("last_edited_time")
|
92
|
+
|
93
|
+
def with_page_size(self, size: int) -> SearchFilterBuilder:
|
94
|
+
"""Set page size for pagination (max 100)."""
|
95
|
+
self.config.page_size = min(size, 100)
|
96
|
+
return self
|
97
|
+
|
98
|
+
def with_cursor(self, cursor: Optional[str]) -> SearchFilterBuilder:
|
99
|
+
"""Set start cursor for pagination."""
|
100
|
+
self.config.start_cursor = cursor
|
101
|
+
return self
|
102
|
+
|
103
|
+
def without_cursor(self) -> SearchFilterBuilder:
|
104
|
+
"""Remove start cursor (for first page)."""
|
105
|
+
self.config.start_cursor = None
|
106
|
+
return self
|
107
|
+
|
108
|
+
def build(self) -> Dict[str, Any]:
|
109
|
+
"""Build the final search filter dictionary. Builder bleibt wiederverwendbar!"""
|
110
|
+
return self.config.to_search_dict()
|
111
|
+
|
112
|
+
def get_config(self) -> SearchConfig:
|
113
|
+
"""Get the underlying SearchConfig."""
|
114
|
+
return self.config
|
115
|
+
|
116
|
+
def copy(self) -> SearchFilterBuilder:
|
117
|
+
"""Create a copy of the builder."""
|
118
|
+
new_config = SearchConfig(
|
119
|
+
query=self.config.query,
|
120
|
+
object_type=self.config.object_type,
|
121
|
+
sort_direction=self.config.sort_direction,
|
122
|
+
sort_timestamp=self.config.sort_timestamp,
|
123
|
+
page_size=self.config.page_size,
|
124
|
+
start_cursor=self.config.start_cursor,
|
125
|
+
)
|
126
|
+
return SearchFilterBuilder(new_config)
|
127
|
+
|
128
|
+
def reset(self) -> SearchFilterBuilder:
|
129
|
+
"""Reset all configurations to defaults."""
|
130
|
+
self.config = SearchConfig()
|
131
|
+
return self
|
notionary/page/utils.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
|
4
|
+
def extract_property_value(prop_data: dict) -> Any:
|
5
|
+
"""
|
6
|
+
Extract the value of a Notion property from its data dict.
|
7
|
+
Supports all common Notion property types.
|
8
|
+
|
9
|
+
Args:
|
10
|
+
prop_data: The property data dictionary from Notion API
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
The extracted value based on property type
|
14
|
+
"""
|
15
|
+
prop_type = prop_data.get("type")
|
16
|
+
if not prop_type:
|
17
|
+
return None
|
18
|
+
|
19
|
+
# Handler dictionary for different property types
|
20
|
+
handlers = {
|
21
|
+
"title": lambda: "".join(
|
22
|
+
t.get("plain_text", "") for t in prop_data.get("title", [])
|
23
|
+
),
|
24
|
+
"rich_text": lambda: "".join(
|
25
|
+
t.get("plain_text", "") for t in prop_data.get("rich_text", [])
|
26
|
+
),
|
27
|
+
"number": lambda: prop_data.get("number"),
|
28
|
+
"select": lambda: (
|
29
|
+
prop_data.get("select", {}).get("name") if prop_data.get("select") else None
|
30
|
+
),
|
31
|
+
"multi_select": lambda: [
|
32
|
+
o.get("name") for o in prop_data.get("multi_select", [])
|
33
|
+
],
|
34
|
+
"status": lambda: (
|
35
|
+
prop_data.get("status", {}).get("name") if prop_data.get("status") else None
|
36
|
+
),
|
37
|
+
"date": lambda: prop_data.get("date"),
|
38
|
+
"checkbox": lambda: prop_data.get("checkbox"),
|
39
|
+
"url": lambda: prop_data.get("url"),
|
40
|
+
"email": lambda: prop_data.get("email"),
|
41
|
+
"phone_number": lambda: prop_data.get("phone_number"),
|
42
|
+
"people": lambda: [p.get("id") for p in prop_data.get("people", [])],
|
43
|
+
"files": lambda: [
|
44
|
+
(
|
45
|
+
f.get("external", {}).get("url")
|
46
|
+
if f.get("type") == "external"
|
47
|
+
else f.get("name")
|
48
|
+
)
|
49
|
+
for f in prop_data.get("files", [])
|
50
|
+
],
|
51
|
+
}
|
52
|
+
|
53
|
+
handler = handlers.get(prop_type)
|
54
|
+
if handler is None:
|
55
|
+
return prop_data # Return raw data if type unknown
|
56
|
+
|
57
|
+
try:
|
58
|
+
return handler()
|
59
|
+
except Exception:
|
60
|
+
return None # Return None if extraction fails
|
notionary/util/__init__.py
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
from .logging_mixin import LoggingMixin
|
2
|
-
from .
|
3
|
-
from .page_id_utils import format_uuid
|
2
|
+
from .singleton_decorator import singleton
|
3
|
+
from .page_id_utils import format_uuid
|
4
|
+
from .fuzzy_matcher import FuzzyMatcher
|
5
|
+
from .factory_decorator import factory_only
|
4
6
|
|
5
|
-
__all__ = [
|
7
|
+
__all__ = [
|
8
|
+
"LoggingMixin",
|
9
|
+
"singleton_decorator",
|
10
|
+
"format_uuid",
|
11
|
+
"FuzzyMatcher",
|
12
|
+
"factory_only",
|
13
|
+
"singleton",
|
14
|
+
]
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import functools
|
2
|
+
import inspect
|
3
|
+
|
4
|
+
|
5
|
+
def factory_only(*allowed_factories):
|
6
|
+
"""
|
7
|
+
Decorator that ensures __init__ is only called from allowed factory methods.
|
8
|
+
|
9
|
+
Args:
|
10
|
+
*allowed_factories: Names of allowed factory methods (e.g. 'from_database_id')
|
11
|
+
"""
|
12
|
+
|
13
|
+
def decorator(init_method):
|
14
|
+
@functools.wraps(init_method)
|
15
|
+
def wrapper(self, *args, **kwargs):
|
16
|
+
frame = inspect.currentframe()
|
17
|
+
try:
|
18
|
+
caller_frame = frame.f_back.f_back
|
19
|
+
if not caller_frame:
|
20
|
+
return init_method(self, *args, **kwargs)
|
21
|
+
caller_name = caller_frame.f_code.co_name
|
22
|
+
if caller_name in allowed_factories or caller_name.startswith("_"):
|
23
|
+
return init_method(self, *args, **kwargs)
|
24
|
+
else:
|
25
|
+
raise RuntimeError(
|
26
|
+
f"Direct instantiation not allowed! Use one of: {', '.join(allowed_factories)}"
|
27
|
+
)
|
28
|
+
finally:
|
29
|
+
del frame
|
30
|
+
|
31
|
+
return wrapper
|
32
|
+
|
33
|
+
return decorator
|
@@ -0,0 +1,82 @@
|
|
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
|
notionary/util/page_id_utils.py
CHANGED
@@ -25,24 +25,3 @@ def format_uuid(value: str) -> Optional[str]:
|
|
25
25
|
if is_valid_uuid(value):
|
26
26
|
return value
|
27
27
|
return extract_uuid(value)
|
28
|
-
|
29
|
-
|
30
|
-
def extract_and_validate_page_id(
|
31
|
-
page_id: Optional[str] = None, url: Optional[str] = None
|
32
|
-
) -> str:
|
33
|
-
if not page_id and not url:
|
34
|
-
raise ValueError("Either page_id or url must be provided")
|
35
|
-
|
36
|
-
candidate = page_id or url
|
37
|
-
|
38
|
-
if is_valid_uuid(candidate):
|
39
|
-
return candidate
|
40
|
-
|
41
|
-
extracted_id = extract_uuid(candidate)
|
42
|
-
if not extracted_id:
|
43
|
-
raise ValueError(f"Could not extract a valid UUID from: {candidate}")
|
44
|
-
|
45
|
-
formatted = format_uuid(extracted_id)
|
46
|
-
if not formatted or not is_valid_uuid(formatted):
|
47
|
-
raise ValueError(f"Invalid UUID format: {formatted}")
|
48
|
-
return formatted
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from typing import TypeVar, Dict, Any, Type, cast
|
2
|
+
|
3
|
+
T = TypeVar("T")
|
4
|
+
|
5
|
+
|
6
|
+
class SingletonMetaClass(type):
|
7
|
+
"""
|
8
|
+
A metaclass that ensures a class has only a single instance.
|
9
|
+
Provides a get_instance() method with proper type hinting.
|
10
|
+
"""
|
11
|
+
|
12
|
+
_instances: Dict[Type, Any] = {}
|
13
|
+
|
14
|
+
def __call__(cls: Type[T], *args: Any, **kwargs: Any) -> T:
|
15
|
+
"""Called when the class is instantiated (e.g., MyClass())."""
|
16
|
+
if cls not in cls._instances:
|
17
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
18
|
+
return cast(T, cls._instances[cls])
|
19
|
+
|
20
|
+
def get_instance(cls: Type[T], *args: Any, **kwargs: Any) -> T:
|
21
|
+
"""Explicit method to retrieve the singleton instance with correct return type."""
|
22
|
+
return cls(*args, **kwargs) # Triggers __call__
|
notionary/workspace.py
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional, List
|
3
|
+
from notionary import NotionPage, NotionDatabase
|
4
|
+
from notionary.database.client import NotionDatabaseClient
|
5
|
+
from notionary.page.client import NotionPageClient
|
6
|
+
from notionary.util import LoggingMixin
|
7
|
+
|
8
|
+
|
9
|
+
class NotionWorkspace(LoggingMixin):
|
10
|
+
"""
|
11
|
+
Represents a Notion workspace, providing methods to interact with databases and pages.
|
12
|
+
"""
|
13
|
+
|
14
|
+
def __init__(self, token: Optional[str] = None):
|
15
|
+
"""
|
16
|
+
Initialize the workspace with a Notion database_client.
|
17
|
+
"""
|
18
|
+
self.database_client = NotionDatabaseClient(token=token)
|
19
|
+
self.page_client = NotionPageClient(token=token)
|
20
|
+
|
21
|
+
async def search_pages(self, query: str, limit=100) -> List[NotionPage]:
|
22
|
+
"""
|
23
|
+
Search for pages globally across Notion workspace.
|
24
|
+
"""
|
25
|
+
response = await self.page_client.search_pages(query, limit=limit)
|
26
|
+
# Parallelisiere die Erzeugung der NotionPage-Instanzen
|
27
|
+
return await asyncio.gather(
|
28
|
+
*(NotionPage.from_page_id(page.id) for page in response.results)
|
29
|
+
)
|
30
|
+
|
31
|
+
async def search_databases(
|
32
|
+
self, query: str, limit: int = 100
|
33
|
+
) -> List[NotionDatabase]:
|
34
|
+
"""
|
35
|
+
Search for databases globally across the Notion workspace.
|
36
|
+
"""
|
37
|
+
response = await self.database_client.search_databases(query=query, limit=limit)
|
38
|
+
return await asyncio.gather(
|
39
|
+
*(
|
40
|
+
NotionDatabase.from_database_id(database.id)
|
41
|
+
for database in response.results
|
42
|
+
)
|
43
|
+
)
|
44
|
+
|
45
|
+
async def get_database_by_name(
|
46
|
+
self, database_name: str
|
47
|
+
) -> Optional[NotionDatabase]:
|
48
|
+
"""
|
49
|
+
Get a Notion database by its name.
|
50
|
+
Uses Notion's search API and returns the first matching database.
|
51
|
+
"""
|
52
|
+
databases = await self.search_databases(query=database_name, limit=1)
|
53
|
+
|
54
|
+
return databases[0] if databases else None
|
55
|
+
|
56
|
+
async def list_all_databases(self, limit: int = 100) -> List[NotionDatabase]:
|
57
|
+
"""
|
58
|
+
List all databases in the workspace.
|
59
|
+
Returns a list of NotionDatabase instances.
|
60
|
+
"""
|
61
|
+
database_results = await self.database_client.search_databases(
|
62
|
+
query="", limit=limit
|
63
|
+
)
|
64
|
+
return [
|
65
|
+
await NotionDatabase.from_database_id(database.id)
|
66
|
+
for database in database_results.results
|
67
|
+
]
|
68
|
+
|
69
|
+
# TODO: Create database would be nice here
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: notionary
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.14
|
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
|
@@ -15,6 +15,9 @@ Requires-Dist: python-dotenv>=1.1.0
|
|
15
15
|
Requires-Dist: pydantic>=2.11.4
|
16
16
|
Requires-Dist: posthog>=3.0.0
|
17
17
|
Requires-Dist: click>=8.0.0
|
18
|
+
Requires-Dist: openai>=1.30.1
|
19
|
+
Requires-Dist: langchain>=0.2.0
|
20
|
+
Requires-Dist: tiktoken>=0.6.0
|
18
21
|
Dynamic: author
|
19
22
|
Dynamic: author-email
|
20
23
|
Dynamic: classifier
|
@@ -0,0 +1,72 @@
|
|
1
|
+
notionary/__init__.py,sha256=4eO6Jx57VRR_Ejo9w7IJeET8SZOvxFl_1lOB39o39No,250
|
2
|
+
notionary/base_notion_client.py,sha256=bqQu9uEdDmZhMAGGv6e_B8mBLOAWLWjoP8s9L6UaQks,6714
|
3
|
+
notionary/workspace.py,sha256=kW9fbVUSECivlvABBwnks2nALfk09V6g6Oc2Eq_pK5U,2511
|
4
|
+
notionary/blocks/__init__.py,sha256=MFBxK3zZ28tV_u8XT20Q6HY39KENCfJDfDflLTYVt4E,2019
|
5
|
+
notionary/blocks/audio_element.py,sha256=rQbWz8akbobci8CFvnFuuHoDNJCG7mcuSXdB8hHjqLU,5355
|
6
|
+
notionary/blocks/bookmark_element.py,sha256=gW6uKCkuWFpHEzq-g1CbvKvma6hyTMUH2XMczI0U-5M,8080
|
7
|
+
notionary/blocks/bulleted_list_element.py,sha256=Uv_ohhF0MTwQ29w_RUX91NFuz57Dtr4vQpV8seRAgy0,2599
|
8
|
+
notionary/blocks/callout_element.py,sha256=Cya-1HIRBiCiyMgQq6PqXU4_iGj2O3qAPirhtC2QrTY,4446
|
9
|
+
notionary/blocks/code_block_element.py,sha256=w0AN5m1qEFEEMDZ5dicCUhh4RwQpjByzDW3PuHgvgt0,7466
|
10
|
+
notionary/blocks/column_element.py,sha256=qzbrTcWWOhGts2fAWHTwQUW8Ca6yoQEMZol9ZxUDCCI,12669
|
11
|
+
notionary/blocks/divider_element.py,sha256=eCX2OupttnjGUaIaF59RhULKqh8R6d8KPnpctMMaXJs,2267
|
12
|
+
notionary/blocks/embed_element.py,sha256=MFHh3ZFNntvaJ1NiEs0bzpbmJTRm0Axqdtf5oputbi0,4516
|
13
|
+
notionary/blocks/heading_element.py,sha256=aALMpclbPTvKfJOICJdgP0y-y7w5jhv7rUQl04TQpeg,3051
|
14
|
+
notionary/blocks/image_element.py,sha256=SEZ31_uDBRy6_lpn8E_GMX5uzI7-c-pJB9idUGZiTrE,4695
|
15
|
+
notionary/blocks/mention_element.py,sha256=G04nnc54YzUP8qu_aAx4-z56fspVrqCc4IHqSw4C5fk,8122
|
16
|
+
notionary/blocks/notion_block_client.py,sha256=mLkJ9mbfTZB7oml2hjXxxmr9XUCfM3u_8xjwKDi77oA,911
|
17
|
+
notionary/blocks/notion_block_element.py,sha256=r27KYICQvdmOg3AyzHE6ouWjX8WuJmX1bERCgkBdaGE,1263
|
18
|
+
notionary/blocks/numbered_list_element.py,sha256=BL_mui9vJ0usOFbRrNZRP_IY8QLG3vGFRYiPPsq_OJw,2596
|
19
|
+
notionary/blocks/paragraph_element.py,sha256=-zCwJOanOVjv07DRArD13yRYaxfL8sCob6oN3PKvzNc,3188
|
20
|
+
notionary/blocks/qoute_element.py,sha256=pkeT6N7PZrepIod8WLrY1DMe2DW6fM98Y4zXiiACenw,6059
|
21
|
+
notionary/blocks/table_element.py,sha256=DzXbSVm3KwTfnLF2cp765gj-VC50zWvj_0RU_WcQDJw,11184
|
22
|
+
notionary/blocks/text_inline_formatter.py,sha256=aKnaR1LvmbBkRdJVId8xtMkrbw1xaw6e4ZLUH97XLfU,8583
|
23
|
+
notionary/blocks/todo_element.py,sha256=6ndhgGJNiy7eb-Ll--Va7zEqQySxFAFYpzY4PWJbGUQ,4059
|
24
|
+
notionary/blocks/toggle_element.py,sha256=2gofKL4ndVkRxkuH-iYVx0YFUc649gpQQbZtwh2BpY8,11017
|
25
|
+
notionary/blocks/toggleable_heading_element.py,sha256=fkXvKtgCg6PuHqrHq7LupmqzpasJ1IyVf2RBLYTiVIo,9893
|
26
|
+
notionary/blocks/video_element.py,sha256=C19XxFRyAUEbhhC9xvhAAGN8YBYP6ON1vm_x7b_gUrY,5664
|
27
|
+
notionary/blocks/prompts/element_prompt_builder.py,sha256=rYMKPmpEFyk26JFZlwcTzMHATpvHnn4Dn284vewFog0,2953
|
28
|
+
notionary/blocks/prompts/element_prompt_content.py,sha256=ItnhGwKsHGnnY9E_LGgZZeTCT9ZfnkJY8xad4wFViWk,1567
|
29
|
+
notionary/blocks/registry/block_registry.py,sha256=hEBa8PdFn1CeevFBqKbcFX7yuBjulwGASUMKoHRsm9s,4305
|
30
|
+
notionary/blocks/registry/block_registry_builder.py,sha256=FA_0WOajaeVaqdphNh8EyN0p_7ItzFqEufYa6YVBLeY,8731
|
31
|
+
notionary/cli/main.py,sha256=-rQoDGvDrFIOvoWzJIIrXQQz4H12D3TkwdNdEF9SEGQ,12883
|
32
|
+
notionary/cli/onboarding.py,sha256=KQornxGBxsyXa0PfVqt4KPq-3B3Ry1sLd5DB3boAB04,3350
|
33
|
+
notionary/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
|
+
notionary/database/client.py,sha256=ZcfydeYlpgGJt6wV1ib33KeXUiL-cGNJ1qraQZ4RVRc,4775
|
35
|
+
notionary/database/database_exceptions.py,sha256=jwFdxoIQHLO3mO3p5t890--1FjbTX60fNyqBAe-sszo,452
|
36
|
+
notionary/database/factory.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
|
+
notionary/database/filter_builder.py,sha256=4EJnWUF73l5oi-HnvMu-mI1OncLzEs2o2mr_xG75quk,6315
|
38
|
+
notionary/database/notion_database.py,sha256=fM6lmL673bKQPfDDj6tyj8K7yO0gSi8veEiUIE5enF8,15497
|
39
|
+
notionary/database/notion_database_provider.py,sha256=2zaRycrbnceV_EbZugdNM_YF9iCGBen-A6E4jvZe2mU,9119
|
40
|
+
notionary/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
|
41
|
+
notionary/elements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
42
|
+
notionary/models/notion_block_response.py,sha256=gzL4C6K9QPcaMS6NbAZaRceSEnMbNwYBVVzxysza5VU,6002
|
43
|
+
notionary/models/notion_database_response.py,sha256=3kvADIP1dSxgITSK4n8Ex3QpF8n_Lxnu_IXbPVGcq4o,7648
|
44
|
+
notionary/models/notion_page_response.py,sha256=7ZwDYhlyK-avix_joQpGuNQZopjlQFI8jS3nvNNumoc,1544
|
45
|
+
notionary/models/search_response.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
46
|
+
notionary/page/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
|
+
notionary/page/client.py,sha256=XQ72lOEwn-gO8fmhKSKHqSHs3hRmoKH0TkJ3TtblcAg,4030
|
48
|
+
notionary/page/markdown_syntax_prompt_generator.py,sha256=uHCPNV9aQi3GzLVimyUKwza29hfxu6DTMVIa_QevJbk,4987
|
49
|
+
notionary/page/notion_page.py,sha256=xxvXJz3wg1TCUyjN6-U6na9zps4fsLlwoVAj3ylBLLA,19151
|
50
|
+
notionary/page/notion_to_markdown_converter.py,sha256=_MJWWwsBvgZ3a8tLZ23ZCIA_G9Qfvt2JG1FqVTlRxHs,6308
|
51
|
+
notionary/page/property_formatter.py,sha256=_978ViH83gfcr-XtDscWTfyBI2srGW2hzC-gzgp5NR8,3788
|
52
|
+
notionary/page/search_filter_builder.py,sha256=wZpW_KHmPXql3sNIyQd9EzZ2-ERy2i0vYNdoLkoBUfc,4597
|
53
|
+
notionary/page/utils.py,sha256=2nfBrWeczBdPH13R3q8dKP4OY4MwEdfKbcs2UJ9kg1o,2041
|
54
|
+
notionary/page/content/notion_page_content_chunker.py,sha256=kWJnV9GLU5YLgSVPKOjwMBbG_CMAmVRkuDtwJYb_UAA,3316
|
55
|
+
notionary/page/content/page_content_retriever.py,sha256=iNazSf0uv_gi0J816-SZn4Lw4qbAxRHG90k9Jy_qw2Q,1587
|
56
|
+
notionary/page/content/page_content_writer.py,sha256=VVvK-Z8NvyIhi7Crcm9mZQuuD_L72NsqSQg9gf33Zwk,7369
|
57
|
+
notionary/page/formatting/markdown_to_notion_converter.py,sha256=9RyGON8VrJv6XifdQdOt5zKgKT3irc974zcbGDBhmLY,17328
|
58
|
+
notionary/page/formatting/spacer_rules.py,sha256=j2RHvdXT3HxXPVBEuCtulyy9cPxsEcOmj71pJqV-D3M,15677
|
59
|
+
notionary/page/properites/property_value_extractor.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
|
+
notionary/util/__init__.py,sha256=EzAmP2uEFfazWapb41BY9kAua1ZEiuLRPaBMtw_cOYg,358
|
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
|
64
|
+
notionary/util/page_id_utils.py,sha256=AA00kRO-g3Cc50tf_XW_tb5RBuPKLuBxRa0D8LYhLXg,736
|
65
|
+
notionary/util/singleton_decorator.py,sha256=CKAvykndwPRZsA3n3MAY_XdCR59MBjjKP0vtm2BcvF0,428
|
66
|
+
notionary/util/singleton_metaclass.py,sha256=uNeHiqS6TwhljvG1RE4NflIp2HyMuMmrCg2xI-vxmHE,809
|
67
|
+
notionary-0.2.14.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
|
68
|
+
notionary-0.2.14.dist-info/METADATA,sha256=R94Tb7hWlk_LhA7bKSbgyLe6K9GgcZstI0WAfaNw1qU,7678
|
69
|
+
notionary-0.2.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
70
|
+
notionary-0.2.14.dist-info/entry_points.txt,sha256=V7X21u3QNm7h7p6Cx0Sx2SO3mtmA7gVwXM8lNYnv9fk,54
|
71
|
+
notionary-0.2.14.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
|
72
|
+
notionary-0.2.14.dist-info/RECORD,,
|