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.
Files changed (82) hide show
  1. notionary/__init__.py +3 -16
  2. notionary/{notion_client.py → base_notion_client.py} +92 -98
  3. notionary/blocks/__init__.py +61 -0
  4. notionary/{elements → blocks}/audio_element.py +6 -3
  5. notionary/{elements → blocks}/bookmark_element.py +3 -5
  6. notionary/{elements → blocks}/bulleted_list_element.py +5 -6
  7. notionary/{elements → blocks}/callout_element.py +4 -6
  8. notionary/{elements → blocks}/code_block_element.py +4 -5
  9. notionary/{elements → blocks}/column_element.py +3 -5
  10. notionary/{elements → blocks}/divider_element.py +3 -5
  11. notionary/{elements → blocks}/embed_element.py +4 -5
  12. notionary/{elements → blocks}/heading_element.py +4 -7
  13. notionary/{elements → blocks}/image_element.py +4 -5
  14. notionary/{elements → blocks}/mention_element.py +3 -6
  15. notionary/blocks/notion_block_client.py +26 -0
  16. notionary/{elements → blocks}/notion_block_element.py +2 -3
  17. notionary/{elements → blocks}/numbered_list_element.py +4 -6
  18. notionary/{elements → blocks}/paragraph_element.py +4 -6
  19. notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
  20. notionary/blocks/prompts/element_prompt_content.py +41 -0
  21. notionary/{elements → blocks}/qoute_element.py +4 -5
  22. notionary/{elements → blocks}/registry/block_registry.py +4 -26
  23. notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
  24. notionary/{elements → blocks}/table_element.py +5 -6
  25. notionary/{elements → blocks}/text_inline_formatter.py +1 -4
  26. notionary/{elements → blocks}/todo_element.py +5 -6
  27. notionary/{elements → blocks}/toggle_element.py +3 -5
  28. notionary/{elements → blocks}/toggleable_heading_element.py +4 -6
  29. notionary/{elements → blocks}/video_element.py +4 -5
  30. notionary/cli/main.py +157 -128
  31. notionary/cli/onboarding.py +10 -9
  32. notionary/database/__init__.py +0 -0
  33. notionary/database/client.py +132 -0
  34. notionary/database/database_exceptions.py +13 -0
  35. notionary/database/factory.py +0 -0
  36. notionary/database/filter_builder.py +175 -0
  37. notionary/database/notion_database.py +339 -126
  38. notionary/database/notion_database_provider.py +230 -0
  39. notionary/elements/__init__.py +0 -0
  40. notionary/models/notion_database_response.py +294 -13
  41. notionary/models/notion_page_response.py +9 -31
  42. notionary/models/search_response.py +0 -0
  43. notionary/page/__init__.py +0 -0
  44. notionary/page/client.py +110 -0
  45. notionary/page/content/page_content_retriever.py +5 -20
  46. notionary/page/content/page_content_writer.py +3 -4
  47. notionary/page/formatting/markdown_to_notion_converter.py +1 -3
  48. notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
  49. notionary/page/notion_page.py +354 -317
  50. notionary/page/notion_to_markdown_converter.py +1 -4
  51. notionary/page/properites/property_value_extractor.py +0 -64
  52. notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
  53. notionary/page/search_filter_builder.py +131 -0
  54. notionary/page/utils.py +60 -0
  55. notionary/util/__init__.py +12 -3
  56. notionary/util/factory_decorator.py +33 -0
  57. notionary/util/fuzzy_matcher.py +82 -0
  58. notionary/util/page_id_utils.py +0 -21
  59. notionary/util/singleton_metaclass.py +22 -0
  60. notionary/workspace.py +69 -0
  61. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
  62. notionary-0.2.14.dist-info/RECORD +72 -0
  63. notionary/database/database_discovery.py +0 -142
  64. notionary/database/notion_database_factory.py +0 -190
  65. notionary/exceptions/database_exceptions.py +0 -76
  66. notionary/exceptions/page_creation_exception.py +0 -9
  67. notionary/page/metadata/metadata_editor.py +0 -150
  68. notionary/page/metadata/notion_icon_manager.py +0 -77
  69. notionary/page/metadata/notion_page_cover_manager.py +0 -56
  70. notionary/page/notion_page_factory.py +0 -328
  71. notionary/page/properites/database_property_service.py +0 -302
  72. notionary/page/properites/page_property_manager.py +0 -152
  73. notionary/page/relations/notion_page_relation_manager.py +0 -350
  74. notionary/page/relations/notion_page_title_resolver.py +0 -104
  75. notionary/page/relations/page_database_relation.py +0 -68
  76. notionary/util/warn_direct_constructor_usage.py +0 -54
  77. notionary-0.2.13.dist-info/RECORD +0 -67
  78. /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
  79. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
  80. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
  81. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
  82. {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.elements.registry.block_registry import BlockRegistry
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(self, property_type: str, value: Any) -> Optional[Dict[str, Any]]:
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
- if self.logger:
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
- return formatter(value)
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
@@ -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
@@ -1,5 +1,14 @@
1
1
  from .logging_mixin import LoggingMixin
2
- from .singleton import singleton
3
- from .page_id_utils import format_uuid, extract_and_validate_page_id
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__ = ["LoggingMixin", "singleton", "format_uuid", "extract_and_validate_page_id"]
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
@@ -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.13
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,,