better-notion 1.4.0__py3-none-any.whl → 1.5.1__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.
@@ -94,7 +94,8 @@ class Database(BaseEntity):
94
94
  *,
95
95
  client: "NotionClient",
96
96
  title: str,
97
- properties: dict[str, Any]
97
+ properties: dict[str, Any] | None = None,
98
+ schema: dict[str, Any] | None = None,
98
99
  ) -> "Database":
99
100
  """Create a new database.
100
101
 
@@ -102,7 +103,8 @@ class Database(BaseEntity):
102
103
  parent: Parent Page
103
104
  client: NotionClient instance
104
105
  title: Database title
105
- properties: Property schema configuration
106
+ properties: Property schema configuration (alias for schema)
107
+ schema: Property schema configuration
106
108
 
107
109
  Returns:
108
110
  Newly created Database object
@@ -112,7 +114,7 @@ class Database(BaseEntity):
112
114
  ... parent=page,
113
115
  ... client=client,
114
116
  ... title="Tasks",
115
- ... properties={
117
+ ... schema={
116
118
  ... "Name": {"type": "title"},
117
119
  ... "Status": {"type": "select"}
118
120
  ... }
@@ -120,13 +122,16 @@ class Database(BaseEntity):
120
122
  """
121
123
  from better_notion._api.properties import Title
122
124
 
123
- # Build title array
124
- title_array = [{"type": "text", "text": {"content": title}}]
125
+ # Support both properties and schema as parameter names
126
+ if schema is not None and properties is None:
127
+ properties = schema
128
+ elif properties is None:
129
+ properties = {}
125
130
 
126
- # Create database via API
131
+ # Create database via API (pass title string - API layer will build the array)
127
132
  data = await client.api.databases.create(
128
133
  parent={"type": "page_id", "page_id": parent.id},
129
- title=title_array,
134
+ title=title,
130
135
  properties=properties
131
136
  )
132
137
 
@@ -15,7 +15,6 @@ Features:
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- import asyncio
19
18
  from pathlib import Path
20
19
  from typing import Optional
21
20
 
@@ -87,6 +86,12 @@ class AgentsPlugin(CombinedPluginInterface):
87
86
  "-n",
88
87
  help="Name for the workspace",
89
88
  ),
89
+ debug: bool = typer.Option(
90
+ False,
91
+ "--debug",
92
+ "-d",
93
+ help="Enable debug logging",
94
+ ),
90
95
  ) -> None:
91
96
  """
92
97
  Initialize a new workspace with all required databases.
@@ -104,6 +109,20 @@ class AgentsPlugin(CombinedPluginInterface):
104
109
  Example:
105
110
  $ notion agents init --parent-page page123 --name "My Workspace"
106
111
  """
112
+ import asyncio
113
+ import logging
114
+ import sys
115
+
116
+ # Enable debug logging if requested
117
+ if debug:
118
+ logging.basicConfig(
119
+ level=logging.DEBUG,
120
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
121
+ stream=sys.stderr,
122
+ )
123
+ # Also enable httpx debug logging
124
+ logging.getLogger("httpx").setLevel(logging.DEBUG)
125
+
107
126
  async def _init() -> str:
108
127
  try:
109
128
  client = get_client()
@@ -126,7 +145,8 @@ class AgentsPlugin(CombinedPluginInterface):
126
145
  )
127
146
 
128
147
  except Exception as e:
129
- return format_error("INIT_ERROR", str(e), retry=False)
148
+ result = format_error("INIT_ERROR", str(e), retry=False)
149
+ return result
130
150
 
131
151
  result = asyncio.run(_init())
132
152
  typer.echo(result)
@@ -672,6 +672,9 @@ def tasks_next(
672
672
  },
673
673
  })
674
674
 
675
+ except ValueError as e:
676
+ # Specific validation error (e.g., project not found)
677
+ return format_error("VALIDATION_ERROR", str(e), retry=False)
675
678
  except Exception as e:
676
679
  return format_error("FIND_NEXT_TASK_ERROR", str(e), retry=False)
677
680
 
@@ -767,20 +770,28 @@ def tasks_start(task_id: str) -> str:
767
770
 
768
771
  def tasks_complete(
769
772
  task_id: str,
770
- actual_hours: Optional[int] = typer.Option(None, "--actual-hours", "-a", help="Actual hours spent"),
773
+ actual_hours: Optional[int] = typer.Option(None, "--actual-hours", "-a", help="Actual hours spent (must be non-negative)"),
771
774
  ) -> str:
772
775
  """
773
776
  Complete a task (transition to Completed).
774
777
 
775
778
  Args:
776
779
  task_id: Task page ID
777
- actual_hours: Actual hours spent (optional)
780
+ actual_hours: Actual hours spent (optional, must be non-negative)
778
781
 
779
782
  Example:
780
783
  $ notion tasks complete task_123 --actual-hours 3
781
784
  """
782
785
  async def _complete() -> str:
783
786
  try:
787
+ # Validate actual_hours parameter if provided
788
+ if actual_hours is not None:
789
+ from better_notion.utils.validators import Validators, ValidationError
790
+ try:
791
+ Validators.non_negative_float(actual_hours, "actual_hours")
792
+ except ValidationError as e:
793
+ return format_error("INVALID_PARAMETER", str(e), retry=False)
794
+
784
795
  client = get_client()
785
796
 
786
797
  # Register SDK plugin
@@ -1047,13 +1058,20 @@ def ideas_review(count: int = 10) -> str:
1047
1058
  Get a batch of ideas for review, prioritized by effort.
1048
1059
 
1049
1060
  Args:
1050
- count: Maximum number of ideas to return
1061
+ count: Maximum number of ideas to return (must be positive)
1051
1062
 
1052
1063
  Example:
1053
1064
  $ notion ideas review --count 5
1054
1065
  """
1055
1066
  async def _review() -> str:
1056
1067
  try:
1068
+ # Validate count parameter
1069
+ from better_notion.utils.validators import Validators, ValidationError
1070
+ try:
1071
+ Validators.positive_int(count, "count")
1072
+ except ValidationError as e:
1073
+ return format_error("INVALID_PARAMETER", str(e), retry=False)
1074
+
1057
1075
  client = get_client()
1058
1076
 
1059
1077
  # Register SDK plugin
@@ -1623,13 +1641,20 @@ def incidents_mttr(project_id: Optional[str] = None, within_days: int = 30) -> s
1623
1641
 
1624
1642
  Args:
1625
1643
  project_id: Filter by project ID (optional)
1626
- within_days: Only consider incidents from last N days
1644
+ within_days: Only consider incidents from last N days (must be positive)
1627
1645
 
1628
1646
  Example:
1629
1647
  $ notion incidents mttr --project-id proj_123 --within-days 30
1630
1648
  """
1631
1649
  async def _mttr() -> str:
1632
1650
  try:
1651
+ # Validate within_days parameter
1652
+ from better_notion.utils.validators import Validators, ValidationError
1653
+ try:
1654
+ Validators.positive_int(within_days, "within_days")
1655
+ except ValidationError as e:
1656
+ return format_error("INVALID_PARAMETER", str(e), retry=False)
1657
+
1633
1658
  client = get_client()
1634
1659
 
1635
1660
  # Register SDK plugin
@@ -400,13 +400,41 @@ class TaskManager:
400
400
 
401
401
  Returns:
402
402
  Task instance or None if no tasks available
403
+
404
+ Raises:
405
+ ValueError: If project_id is provided but project doesn't exist
403
406
  """
404
- from better_notion.plugins.official.agents_sdk.models import Task
407
+ from better_notion.plugins.official.agents_sdk.models import Task, Project
405
408
 
406
409
  database_id = self._get_database_id("Tasks")
407
410
  if not database_id:
408
411
  return None
409
412
 
413
+ # Validate project_id if provided
414
+ if project_id:
415
+ # First check if Projects database exists in workspace config
416
+ projects_db = self._get_database_id("Projects")
417
+ if not projects_db:
418
+ # Workspace not initialized, can't validate project
419
+ pass
420
+ else:
421
+ # Check if project exists by querying for it
422
+ try:
423
+ project_response = await self._client._api.databases.query(
424
+ database_id=projects_db,
425
+ filter={"property": "id", "rich_text": {"equals": project_id}}
426
+ )
427
+ # If no results, project doesn't exist
428
+ if not project_response.get("results"):
429
+ raise ValueError(
430
+ f"Project '{project_id}' not found. "
431
+ f"Please verify the project ID or run 'notion agents projects list' to see available projects."
432
+ )
433
+ except Exception as e:
434
+ if "not found" in str(e).lower():
435
+ raise
436
+ # If query fails for other reasons, continue without validation
437
+
410
438
  # Filter for backlog/claimed tasks
411
439
  response = await self._client._api.databases.query(
412
440
  database_id=database_id,
@@ -50,159 +50,174 @@ class SelectOption:
50
50
 
51
51
 
52
52
  class PropertyBuilder:
53
- """Helper class for building Notion database properties."""
53
+ """
54
+ Helper class for building Notion database properties.
55
+
56
+ Notion API create format:
57
+ "Name": {"title": {}}
58
+ "Status": {"select": {"options": [...]}}
59
+ """
54
60
 
55
61
  @staticmethod
56
- def title(name: str = "Name") -> Dict[str, str]:
57
- """Create a title property.
62
+ def title(name: str = "Name") -> Dict[str, Any]:
63
+ """
64
+ Create a title property.
58
65
 
59
66
  Args:
60
67
  name: Property name (default: "Name")
61
68
 
62
69
  Returns:
63
- Title property schema
70
+ Title property schema for Notion API create
64
71
  """
65
- return {"type": "title", "name": name}
72
+ return {"title": {}}
66
73
 
67
74
  @staticmethod
68
- def text(name: str) -> Dict[str, str]:
69
- """Create a text property.
75
+ def text(name: str) -> Dict[str, Any]:
76
+ """
77
+ Create a text property.
70
78
 
71
79
  Args:
72
80
  name: Property name
73
81
 
74
82
  Returns:
75
- Text property schema
83
+ Text property schema for Notion API create
76
84
  """
77
- return {"type": "rich_text", "name": name}
85
+ return {"rich_text": {}}
78
86
 
79
87
  @staticmethod
80
88
  def number(name: str, format: Optional[str] = None) -> Dict[str, Any]:
81
- """Create a number property.
89
+ """
90
+ Create a number property.
82
91
 
83
92
  Args:
84
93
  name: Property name
85
94
  format: Number format (number, percent, dollar, euro, pound, yen, ruble)
86
95
 
87
96
  Returns:
88
- Number property schema
97
+ Number property schema for Notion API create
89
98
  """
90
- prop: Dict[str, Any] = {"type": "number", "name": name}
91
-
92
99
  if format:
93
- prop["number"] = {"format": format}
94
-
95
- return prop
100
+ return {"number": {"format": format}}
101
+ return {"number": {}}
96
102
 
97
103
  @staticmethod
98
104
  def select(name: str, options: List[Dict[str, str]]) -> Dict[str, Any]:
99
- """Create a select property.
105
+ """
106
+ Create a select property.
100
107
 
101
108
  Args:
102
109
  name: Property name
103
110
  options: List of option dicts from SelectOption.option()
104
111
 
105
112
  Returns:
106
- Select property schema
113
+ Select property schema for Notion API create
107
114
  """
108
- return {"type": "select", "name": name, "select": {"options": options}}
115
+ return {"select": {"options": options}}
109
116
 
110
117
  @staticmethod
111
118
  def multi_select(name: str, options: List[Dict[str, str]]) -> Dict[str, Any]:
112
- """Create a multi-select property.
119
+ """
120
+ Create a multi-select property.
113
121
 
114
122
  Args:
115
123
  name: Property name
116
124
  options: List of option dicts
117
125
 
118
126
  Returns:
119
- Multi-select property schema
127
+ Multi-select property schema for Notion API create
120
128
  """
121
- return {"type": "multi_select", "name": name, "multi_select": {"options": options}}
129
+ return {"multi_select": {"options": options}}
122
130
 
123
131
  @staticmethod
124
- def date(name: str) -> Dict[str, str]:
125
- """Create a date property.
132
+ def date(name: str) -> Dict[str, Any]:
133
+ """
134
+ Create a date property.
126
135
 
127
136
  Args:
128
137
  name: Property name
129
138
 
130
139
  Returns:
131
- Date property schema
140
+ Date property schema for Notion API create
132
141
  """
133
- return {"type": "date", "name": name}
142
+ return {"date": {}}
134
143
 
135
144
  @staticmethod
136
- def checkbox(name: str) -> Dict[str, str]:
137
- """Create a checkbox property.
145
+ def checkbox(name: str) -> Dict[str, Any]:
146
+ """
147
+ Create a checkbox property.
138
148
 
139
149
  Args:
140
150
  name: Property name
141
151
 
142
152
  Returns:
143
- Checkbox property schema
153
+ Checkbox property schema for Notion API create
144
154
  """
145
- return {"type": "checkbox", "name": name}
155
+ return {"checkbox": {}}
146
156
 
147
157
  @staticmethod
148
- def url(name: str) -> Dict[str, str]:
149
- """Create a URL property.
158
+ def url(name: str) -> Dict[str, Any]:
159
+ """
160
+ Create a URL property.
150
161
 
151
162
  Args:
152
163
  name: Property name
153
164
 
154
165
  Returns:
155
- URL property schema
166
+ URL property schema for Notion API create
156
167
  """
157
- return {"type": "url", "name": name}
168
+ return {"url": {}}
158
169
 
159
170
  @staticmethod
160
- def email(name: str) -> Dict[str, str]:
161
- """Create an email property.
171
+ def email(name: str) -> Dict[str, Any]:
172
+ """
173
+ Create an email property.
162
174
 
163
175
  Args:
164
176
  name: Property name
165
177
 
166
178
  Returns:
167
- Email property schema
179
+ Email property schema for Notion API create
168
180
  """
169
- return {"type": "email", "name": name}
181
+ return {"email": {}}
170
182
 
171
183
  @staticmethod
172
- def phone(name: str) -> Dict[str, str]:
173
- """Create a phone property.
184
+ def phone(name: str) -> Dict[str, Any]:
185
+ """
186
+ Create a phone property.
174
187
 
175
188
  Args:
176
189
  name: Property name
177
190
 
178
191
  Returns:
179
- Phone property schema
192
+ Phone property schema for Notion API create
180
193
  """
181
- return {"type": "phone", "name": name}
194
+ return {"phone_number": {}}
182
195
 
183
196
  @staticmethod
184
- def people(name: str) -> Dict[str, str]:
185
- """Create a people property.
197
+ def people(name: str) -> Dict[str, Any]:
198
+ """
199
+ Create a people property.
186
200
 
187
201
  Args:
188
202
  name: Property name
189
203
 
190
204
  Returns:
191
- People property schema
205
+ People property schema for Notion API create
192
206
  """
193
- return {"type": "people", "name": name}
207
+ return {"people": {}}
194
208
 
195
209
  @staticmethod
196
- def files(name: str) -> Dict[str, str]:
197
- """Create a files property.
210
+ def files(name: str) -> Dict[str, Any]:
211
+ """
212
+ Create a files property.
198
213
 
199
214
  Args:
200
215
  name: Property name
201
216
 
202
217
  Returns:
203
- Files property schema
218
+ Files property schema for Notion API create
204
219
  """
205
- return {"type": "files", "name": name}
220
+ return {"files": {}}
206
221
 
207
222
  @staticmethod
208
223
  def relation(
@@ -210,7 +225,8 @@ class PropertyBuilder:
210
225
  database_id: Optional[str] = None,
211
226
  dual_property: bool = True,
212
227
  ) -> Dict[str, Any]:
213
- """Create a relation property.
228
+ """
229
+ Create a relation property.
214
230
 
215
231
  Args:
216
232
  name: Property name
@@ -224,19 +240,22 @@ class PropertyBuilder:
224
240
  If database_id is None, it must be set later when the related
225
241
  database is created.
226
242
  """
227
- prop: Dict[str, Any] = {"type": "relation", "name": name, "relation": {}}
243
+ relation_config: Dict[str, Any] = {}
228
244
 
229
245
  if database_id:
230
- prop["relation"]["database_id"] = database_id
246
+ relation_config["database_id"] = database_id
231
247
 
232
248
  if dual_property:
233
- prop["relation"]["type"] = "dual_property"
249
+ relation_config["dual_property"] = {}
250
+ else:
251
+ relation_config["single_property"] = {}
234
252
 
235
- return prop
253
+ return {"relation": relation_config}
236
254
 
237
255
  @staticmethod
238
256
  def formula(name: str, expression: str) -> Dict[str, Any]:
239
- """Create a formula property.
257
+ """
258
+ Create a formula property.
240
259
 
241
260
  Args:
242
261
  name: Property name
@@ -245,11 +264,12 @@ class PropertyBuilder:
245
264
  Returns:
246
265
  Formula property schema
247
266
  """
248
- return {"type": "formula", "name": name, "formula": {"expression": expression}}
267
+ return {"formula": {"expression": expression}}
249
268
 
250
269
  @staticmethod
251
- def created_time(name: str = "Created time") -> Dict[str, str]:
252
- """Create a created_time property.
270
+ def created_time(name: str = "Created time") -> Dict[str, Any]:
271
+ """
272
+ Create a created_time property.
253
273
 
254
274
  Args:
255
275
  name: Property name (default: "Created time")
@@ -257,11 +277,12 @@ class PropertyBuilder:
257
277
  Returns:
258
278
  Created time property schema
259
279
  """
260
- return {"type": "created_time", "name": name}
280
+ return {"created_time": {}}
261
281
 
262
282
  @staticmethod
263
- def created_by(name: str = "Created by") -> Dict[str, str]:
264
- """Create a created_by property.
283
+ def created_by(name: str = "Created by") -> Dict[str, Any]:
284
+ """
285
+ Create a created_by property.
265
286
 
266
287
  Args:
267
288
  name: Property name (default: "Created by")
@@ -269,7 +290,7 @@ class PropertyBuilder:
269
290
  Returns:
270
291
  Created by property schema
271
292
  """
272
- return {"type": "created_by", "name": name}
293
+ return {"created_by": {}}
273
294
 
274
295
 
275
296
  class OrganizationSchema:
@@ -277,7 +298,8 @@ class OrganizationSchema:
277
298
 
278
299
  @staticmethod
279
300
  def get_schema() -> Dict[str, Dict[str, Any]]:
280
- """Return Notion database schema for Organizations.
301
+ """
302
+ Return Notion database schema for Organizations.
281
303
 
282
304
  Returns:
283
305
  Dict mapping property names to property schemas
@@ -332,7 +354,7 @@ class ProjectSchema:
332
354
  SelectOption.option("React", "blue"),
333
355
  SelectOption.option("Vue", "green"),
334
356
  SelectOption.option("Node.js", "green"),
335
- SelectOption.option("Go", "cyan"),
357
+ SelectOption.option("Go", "blue"),
336
358
  SelectOption.option("Rust", "orange"),
337
359
  SelectOption.option("Java", "red"),
338
360
  SelectOption.option("C++", "blue"),
@@ -389,7 +411,8 @@ class VersionSchema:
389
411
  "Branch Name": PropertyBuilder.text("Branch Name"),
390
412
  "Progress": PropertyBuilder.number("Progress", format="percent"),
391
413
  "Release Date": PropertyBuilder.date("Release Date"),
392
- "Superseded By": PropertyBuilder.relation("Superseded By", dual_property=False),
414
+ # Note: "Superseded By" self-referential relation removed because
415
+ # it can't be created during initial database creation (needs its own ID)
393
416
  }
394
417
 
395
418
 
@@ -435,8 +458,8 @@ class TaskSchema:
435
458
  SelectOption.option("Low", "blue"),
436
459
  ],
437
460
  ),
438
- "Dependencies": PropertyBuilder.relation("Dependencies", dual_property=False),
439
- "Dependent Tasks": PropertyBuilder.relation("Dependent Tasks", dual_property=False),
461
+ # Note: Self-referential relations (Dependencies, Dependent Tasks) removed because
462
+ # they can't be created during initial database creation (need the database's own ID)
440
463
  "Estimated Hours": PropertyBuilder.number("Estimated Hours"),
441
464
  "Actual Hours": PropertyBuilder.number("Actual Hours"),
442
465
  "Assignee": PropertyBuilder.people("Assignee"),