tracktolib 0.62.1__tar.gz → 0.64.0__tar.gz
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.
- {tracktolib-0.62.1 → tracktolib-0.64.0}/PKG-INFO +3 -1
- {tracktolib-0.62.1 → tracktolib-0.64.0}/pyproject.toml +5 -4
- tracktolib-0.64.0/tracktolib/notion/fetch.py +352 -0
- tracktolib-0.64.0/tracktolib/notion/models.py +281 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/pg/query.py +98 -14
- tracktolib-0.64.0/tracktolib/s3/__init__.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/README.md +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/__init__.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/api.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/http_utils.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/logs.py +0 -0
- {tracktolib-0.62.1/tracktolib/s3 → tracktolib-0.64.0/tracktolib/notion}/__init__.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/pg/__init__.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/pg/utils.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/pg_sync.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/pg_utils.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/s3/minio.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/s3/s3.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/tests.py +0 -0
- {tracktolib-0.62.1 → tracktolib-0.64.0}/tracktolib/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tracktolib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.64.0
|
|
4
4
|
Summary: Utility library for python
|
|
5
5
|
Keywords: utility
|
|
6
6
|
Author-email: julien.brayere@tracktor.fr
|
|
@@ -10,6 +10,7 @@ Requires-Dist: fastapi>=0.103.2 ; extra == 'api'
|
|
|
10
10
|
Requires-Dist: pydantic>=2 ; extra == 'api'
|
|
11
11
|
Requires-Dist: httpx>=0.25.0 ; extra == 'http'
|
|
12
12
|
Requires-Dist: python-json-logger>=3.2.1 ; extra == 'logs'
|
|
13
|
+
Requires-Dist: niquests>=3.15.2 ; extra == 'notion'
|
|
13
14
|
Requires-Dist: asyncpg>=0.27.0 ; extra == 'pg'
|
|
14
15
|
Requires-Dist: rich>=13.6.0 ; extra == 'pg'
|
|
15
16
|
Requires-Dist: psycopg>=3.1.12 ; extra == 'pg-sync'
|
|
@@ -21,6 +22,7 @@ Requires-Python: >=3.12, <4.0
|
|
|
21
22
|
Provides-Extra: api
|
|
22
23
|
Provides-Extra: http
|
|
23
24
|
Provides-Extra: logs
|
|
25
|
+
Provides-Extra: notion
|
|
24
26
|
Provides-Extra: pg
|
|
25
27
|
Provides-Extra: pg-sync
|
|
26
28
|
Provides-Extra: s3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tracktolib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.64.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ email = "julien.brayere@tracktor.fr" }
|
|
6
6
|
]
|
|
@@ -18,8 +18,6 @@ include = [
|
|
|
18
18
|
]
|
|
19
19
|
requires-python = ">=3.12,<4.0"
|
|
20
20
|
|
|
21
|
-
dependencies = []
|
|
22
|
-
|
|
23
21
|
[project.optional-dependencies]
|
|
24
22
|
logs = ["python-json-logger>=3.2.1"]
|
|
25
23
|
pg-sync = ["psycopg>=3.1.12"]
|
|
@@ -39,6 +37,9 @@ pg = [
|
|
|
39
37
|
"asyncpg>=0.27.0",
|
|
40
38
|
"rich>=13.6.0",
|
|
41
39
|
]
|
|
40
|
+
notion = [
|
|
41
|
+
"niquests>=3.15.2"
|
|
42
|
+
]
|
|
42
43
|
|
|
43
44
|
[dependency-groups]
|
|
44
45
|
dev = [
|
|
@@ -91,7 +92,7 @@ pythonPlatform = "Linux"
|
|
|
91
92
|
|
|
92
93
|
[tool.commitizen]
|
|
93
94
|
name = "cz_conventional_commits"
|
|
94
|
-
version = "0.
|
|
95
|
+
version = "0.64.0"
|
|
95
96
|
tag_format = "$version"
|
|
96
97
|
version_files = [
|
|
97
98
|
"pyproject.toml:version"
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import niquests
|
|
6
|
+
except ImportError:
|
|
7
|
+
raise ImportError('Please install niquests or tracktolib with "notion" to use this module')
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
Block,
|
|
11
|
+
BlockListResponse,
|
|
12
|
+
Database,
|
|
13
|
+
IntrospectTokenResponse,
|
|
14
|
+
Page,
|
|
15
|
+
PageListResponse,
|
|
16
|
+
RevokeTokenResponse,
|
|
17
|
+
SearchResponse,
|
|
18
|
+
TokenResponse,
|
|
19
|
+
User,
|
|
20
|
+
UserListResponse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
# Auth helpers
|
|
25
|
+
"get_notion_headers",
|
|
26
|
+
# OAuth
|
|
27
|
+
"create_token",
|
|
28
|
+
"introspect_token",
|
|
29
|
+
"revoke_token",
|
|
30
|
+
"refresh_token",
|
|
31
|
+
# Users
|
|
32
|
+
"fetch_users",
|
|
33
|
+
"fetch_user",
|
|
34
|
+
"fetch_me",
|
|
35
|
+
# Pages
|
|
36
|
+
"fetch_page",
|
|
37
|
+
"create_page",
|
|
38
|
+
"update_page",
|
|
39
|
+
# Databases
|
|
40
|
+
"fetch_database",
|
|
41
|
+
"query_database",
|
|
42
|
+
# Blocks
|
|
43
|
+
"fetch_block",
|
|
44
|
+
"fetch_block_children",
|
|
45
|
+
"fetch_append_block_children",
|
|
46
|
+
# Search
|
|
47
|
+
"fetch_search",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
NOTION_API_URL = "https://api.notion.com"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_notion_token() -> str:
|
|
54
|
+
"""Get Notion token from config or environment."""
|
|
55
|
+
token = os.environ.get("NOTION_TOKEN")
|
|
56
|
+
if not token:
|
|
57
|
+
raise ValueError("Notion token not found. Set NOTION_TOKEN env var.")
|
|
58
|
+
return token
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_notion_headers(api_version: str = "2025-09-03", token: str | None = None):
|
|
62
|
+
"""Get headers for Notion API requests."""
|
|
63
|
+
_token = token or _get_notion_token()
|
|
64
|
+
return {
|
|
65
|
+
"Authorization": f"Bearer {_token}",
|
|
66
|
+
"Notion-Version": api_version,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# OAuth endpoints
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def create_token(
|
|
74
|
+
session: niquests.AsyncSession,
|
|
75
|
+
client_id: str,
|
|
76
|
+
client_secret: str,
|
|
77
|
+
code: str,
|
|
78
|
+
redirect_uri: str | None = None,
|
|
79
|
+
) -> TokenResponse:
|
|
80
|
+
"""Create an access token from an OAuth authorization code."""
|
|
81
|
+
payload: dict[str, str] = {
|
|
82
|
+
"grant_type": "authorization_code",
|
|
83
|
+
"code": code,
|
|
84
|
+
}
|
|
85
|
+
if redirect_uri:
|
|
86
|
+
payload["redirect_uri"] = redirect_uri
|
|
87
|
+
|
|
88
|
+
response = await session.post(f"{NOTION_API_URL}/v1/oauth/token", json=payload, auth=(client_id, client_secret))
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
return response.json() # type: ignore[return-value]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def introspect_token(
|
|
94
|
+
session: niquests.AsyncSession,
|
|
95
|
+
client_id: str,
|
|
96
|
+
client_secret: str,
|
|
97
|
+
token: str,
|
|
98
|
+
) -> IntrospectTokenResponse:
|
|
99
|
+
"""Get a token's active status, scope, and issued time."""
|
|
100
|
+
payload = {"token": token}
|
|
101
|
+
response = await session.post(
|
|
102
|
+
f"{NOTION_API_URL}/v1/oauth/introspect", json=payload, auth=(client_id, client_secret)
|
|
103
|
+
)
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
return response.json() # type: ignore[return-value]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def revoke_token(
|
|
109
|
+
session: niquests.AsyncSession,
|
|
110
|
+
client_id: str,
|
|
111
|
+
client_secret: str,
|
|
112
|
+
token: str,
|
|
113
|
+
) -> RevokeTokenResponse:
|
|
114
|
+
"""Revoke an access token."""
|
|
115
|
+
payload = {"token": token}
|
|
116
|
+
|
|
117
|
+
response = await session.post(f"{NOTION_API_URL}/v1/oauth/revoke", json=payload, auth=(client_id, client_secret))
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
return response.json() # type: ignore[return-value]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def refresh_token(
|
|
123
|
+
session: niquests.AsyncSession,
|
|
124
|
+
client_id: str,
|
|
125
|
+
client_secret: str,
|
|
126
|
+
refresh_token_value: str,
|
|
127
|
+
) -> TokenResponse:
|
|
128
|
+
"""Refresh an access token, generating new access and refresh tokens."""
|
|
129
|
+
payload = {
|
|
130
|
+
"grant_type": "refresh_token",
|
|
131
|
+
"refresh_token": refresh_token_value,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
response = await session.post(f"{NOTION_API_URL}/v1/oauth/token", json=payload, auth=(client_id, client_secret))
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
return response.json() # type: ignore[return-value]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Users endpoints
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def fetch_users(
|
|
143
|
+
session: niquests.AsyncSession,
|
|
144
|
+
*,
|
|
145
|
+
start_cursor: str | None = None,
|
|
146
|
+
page_size: int | None = None,
|
|
147
|
+
) -> UserListResponse:
|
|
148
|
+
"""List all users in the workspace."""
|
|
149
|
+
params: dict[str, str] = {}
|
|
150
|
+
if start_cursor:
|
|
151
|
+
params["start_cursor"] = start_cursor
|
|
152
|
+
if page_size:
|
|
153
|
+
params["page_size"] = str(page_size)
|
|
154
|
+
|
|
155
|
+
response = await session.get(f"{NOTION_API_URL}/v1/users", params=params or None)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
return response.json() # type: ignore[return-value]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def fetch_user(session: niquests.AsyncSession, user_id: str) -> User:
|
|
161
|
+
"""Retrieve a user by ID."""
|
|
162
|
+
response = await session.get(f"{NOTION_API_URL}/v1/users/{user_id}")
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
return response.json() # type: ignore[return-value]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def fetch_me(session: niquests.AsyncSession) -> User:
|
|
168
|
+
"""Retrieve the bot user associated with the token."""
|
|
169
|
+
response = await session.get(f"{NOTION_API_URL}/v1/users/me")
|
|
170
|
+
response.raise_for_status()
|
|
171
|
+
return response.json() # type: ignore[return-value]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Pages endpoints
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def fetch_page(session: niquests.AsyncSession, page_id: str) -> Page:
|
|
178
|
+
"""Retrieve a page by ID."""
|
|
179
|
+
response = await session.get(f"{NOTION_API_URL}/v1/pages/{page_id}")
|
|
180
|
+
response.raise_for_status()
|
|
181
|
+
return response.json() # type: ignore[return-value]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def create_page(
|
|
185
|
+
session: niquests.AsyncSession,
|
|
186
|
+
*,
|
|
187
|
+
parent: dict[str, Any],
|
|
188
|
+
properties: dict[str, Any],
|
|
189
|
+
children: list[dict[str, Any]] | None = None,
|
|
190
|
+
icon: dict[str, Any] | None = None,
|
|
191
|
+
cover: dict[str, Any] | None = None,
|
|
192
|
+
) -> Page:
|
|
193
|
+
"""Create a new page."""
|
|
194
|
+
payload: dict[str, Any] = {
|
|
195
|
+
"parent": parent,
|
|
196
|
+
"properties": properties,
|
|
197
|
+
}
|
|
198
|
+
if children:
|
|
199
|
+
payload["children"] = children
|
|
200
|
+
if icon:
|
|
201
|
+
payload["icon"] = icon
|
|
202
|
+
if cover:
|
|
203
|
+
payload["cover"] = cover
|
|
204
|
+
|
|
205
|
+
response = await session.post(f"{NOTION_API_URL}/v1/pages", json=payload)
|
|
206
|
+
response.raise_for_status()
|
|
207
|
+
return response.json() # type: ignore[return-value]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def update_page(
|
|
211
|
+
session: niquests.AsyncSession,
|
|
212
|
+
page_id: str,
|
|
213
|
+
*,
|
|
214
|
+
properties: dict[str, Any] | None = None,
|
|
215
|
+
archived: bool | None = None,
|
|
216
|
+
icon: dict[str, Any] | None = None,
|
|
217
|
+
cover: dict[str, Any] | None = None,
|
|
218
|
+
) -> Page:
|
|
219
|
+
"""Update a page's properties."""
|
|
220
|
+
payload: dict[str, Any] = {}
|
|
221
|
+
if properties is not None:
|
|
222
|
+
payload["properties"] = properties
|
|
223
|
+
if archived is not None:
|
|
224
|
+
payload["archived"] = archived
|
|
225
|
+
if icon is not None:
|
|
226
|
+
payload["icon"] = icon
|
|
227
|
+
if cover is not None:
|
|
228
|
+
payload["cover"] = cover
|
|
229
|
+
|
|
230
|
+
response = await session.patch(f"{NOTION_API_URL}/v1/pages/{page_id}", json=payload)
|
|
231
|
+
response.raise_for_status()
|
|
232
|
+
return response.json() # type: ignore[return-value]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# Databases endpoints
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def fetch_database(session: niquests.AsyncSession, database_id: str) -> Database:
|
|
239
|
+
"""Retrieve a database by ID."""
|
|
240
|
+
response = await session.get(f"{NOTION_API_URL}/v1/databases/{database_id}")
|
|
241
|
+
response.raise_for_status()
|
|
242
|
+
return response.json() # type: ignore[return-value]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def query_database(
|
|
246
|
+
session: niquests.AsyncSession,
|
|
247
|
+
database_id: str,
|
|
248
|
+
*,
|
|
249
|
+
filter: dict[str, Any] | None = None,
|
|
250
|
+
sorts: list[dict[str, Any]] | None = None,
|
|
251
|
+
start_cursor: str | None = None,
|
|
252
|
+
page_size: int | None = None,
|
|
253
|
+
) -> PageListResponse:
|
|
254
|
+
"""Query a database."""
|
|
255
|
+
payload: dict[str, Any] = {}
|
|
256
|
+
if filter:
|
|
257
|
+
payload["filter"] = filter
|
|
258
|
+
if sorts:
|
|
259
|
+
payload["sorts"] = sorts
|
|
260
|
+
if start_cursor:
|
|
261
|
+
payload["start_cursor"] = start_cursor
|
|
262
|
+
if page_size:
|
|
263
|
+
payload["page_size"] = page_size
|
|
264
|
+
|
|
265
|
+
response = await session.post(f"{NOTION_API_URL}/v1/databases/{database_id}/query", json=payload or None)
|
|
266
|
+
response.raise_for_status()
|
|
267
|
+
return response.json() # type: ignore[return-value]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Blocks endpoints
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
async def fetch_block(session: niquests.AsyncSession, block_id: str) -> Block:
|
|
274
|
+
"""Retrieve a block by ID."""
|
|
275
|
+
response = await session.get(f"{NOTION_API_URL}/v1/blocks/{block_id}")
|
|
276
|
+
response.raise_for_status()
|
|
277
|
+
return response.json() # type: ignore[return-value]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def fetch_block_children(
|
|
281
|
+
session: niquests.AsyncSession,
|
|
282
|
+
block_id: str,
|
|
283
|
+
*,
|
|
284
|
+
start_cursor: str | None = None,
|
|
285
|
+
page_size: int | None = None,
|
|
286
|
+
) -> BlockListResponse:
|
|
287
|
+
"""Retrieve a block's children."""
|
|
288
|
+
params: dict[str, str] = {}
|
|
289
|
+
if start_cursor:
|
|
290
|
+
params["start_cursor"] = start_cursor
|
|
291
|
+
if page_size:
|
|
292
|
+
params["page_size"] = str(page_size)
|
|
293
|
+
|
|
294
|
+
response = await session.get(f"{NOTION_API_URL}/v1/blocks/{block_id}/children", params=params or None)
|
|
295
|
+
response.raise_for_status()
|
|
296
|
+
return response.json() # type: ignore[return-value]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def fetch_append_block_children(
|
|
300
|
+
session: niquests.AsyncSession,
|
|
301
|
+
block_id: str,
|
|
302
|
+
children: list[dict[str, Any]],
|
|
303
|
+
) -> BlockListResponse:
|
|
304
|
+
"""Append children blocks to a parent block."""
|
|
305
|
+
payload = {"children": children}
|
|
306
|
+
|
|
307
|
+
response = await session.patch(f"{NOTION_API_URL}/v1/blocks/{block_id}/children", json=payload)
|
|
308
|
+
response.raise_for_status()
|
|
309
|
+
return response.json() # type: ignore[return-value]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Search endpoint
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def fetch_search(
|
|
316
|
+
session: niquests.AsyncSession,
|
|
317
|
+
*,
|
|
318
|
+
query: str | None = None,
|
|
319
|
+
filter: dict[str, Any] | None = None,
|
|
320
|
+
sort: dict[str, Any] | None = None,
|
|
321
|
+
start_cursor: str | None = None,
|
|
322
|
+
page_size: int | None = None,
|
|
323
|
+
) -> SearchResponse:
|
|
324
|
+
"""Search pages and databases."""
|
|
325
|
+
payload: dict[str, Any] = {}
|
|
326
|
+
if query:
|
|
327
|
+
payload["query"] = query
|
|
328
|
+
if filter:
|
|
329
|
+
payload["filter"] = filter
|
|
330
|
+
if sort:
|
|
331
|
+
payload["sort"] = sort
|
|
332
|
+
if start_cursor:
|
|
333
|
+
payload["start_cursor"] = start_cursor
|
|
334
|
+
if page_size:
|
|
335
|
+
payload["page_size"] = page_size
|
|
336
|
+
|
|
337
|
+
response = await session.post(f"{NOTION_API_URL}/v1/search", json=payload or None)
|
|
338
|
+
response.raise_for_status()
|
|
339
|
+
return response.json() # type: ignore[return-value]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
import asyncio
|
|
344
|
+
|
|
345
|
+
async def main():
|
|
346
|
+
async with niquests.AsyncSession() as session:
|
|
347
|
+
session.headers.update(get_notion_headers())
|
|
348
|
+
me = await fetch_me(session)
|
|
349
|
+
print("Me:", me)
|
|
350
|
+
# print(await fetch_search(session, filter="aaa"))
|
|
351
|
+
|
|
352
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Notion API models based on official notion-sdk-js types."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
4
|
+
|
|
5
|
+
# Base types
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PartialUser(TypedDict):
|
|
9
|
+
"""Partial user object."""
|
|
10
|
+
|
|
11
|
+
id: str
|
|
12
|
+
object: Literal["user"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PersonDetails(TypedDict):
|
|
16
|
+
"""Person details."""
|
|
17
|
+
|
|
18
|
+
email: NotRequired[str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PersonUserObjectResponse(TypedDict):
|
|
22
|
+
"""Person user type details."""
|
|
23
|
+
|
|
24
|
+
type: Literal["person"]
|
|
25
|
+
person: PersonDetails
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BotUserObjectResponse(TypedDict):
|
|
29
|
+
"""Bot user type details."""
|
|
30
|
+
|
|
31
|
+
type: Literal["bot"]
|
|
32
|
+
bot: dict[str, Any]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UserObjectResponseCommon(TypedDict):
|
|
36
|
+
"""Common user object fields."""
|
|
37
|
+
|
|
38
|
+
id: str
|
|
39
|
+
object: Literal["user"]
|
|
40
|
+
name: str | None
|
|
41
|
+
avatar_url: str | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# User object is common fields + either person or bot
|
|
45
|
+
class User(UserObjectResponseCommon):
|
|
46
|
+
"""Full user object."""
|
|
47
|
+
|
|
48
|
+
type: Literal["person", "bot"]
|
|
49
|
+
person: NotRequired[PersonDetails]
|
|
50
|
+
bot: NotRequired[dict[str, Any]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# OAuth types
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UserOwner(TypedDict):
|
|
57
|
+
"""User owner for OAuth."""
|
|
58
|
+
|
|
59
|
+
type: Literal["user"]
|
|
60
|
+
user: User | PartialUser
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class WorkspaceOwner(TypedDict):
|
|
64
|
+
"""Workspace owner for OAuth."""
|
|
65
|
+
|
|
66
|
+
type: Literal["workspace"]
|
|
67
|
+
workspace: Literal[True]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
Owner = UserOwner | WorkspaceOwner
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TokenResponse(TypedDict):
|
|
74
|
+
"""Response from creating or refreshing an access token."""
|
|
75
|
+
|
|
76
|
+
access_token: str
|
|
77
|
+
token_type: Literal["bearer"]
|
|
78
|
+
refresh_token: str | None
|
|
79
|
+
bot_id: str
|
|
80
|
+
workspace_icon: str | None
|
|
81
|
+
workspace_name: str | None
|
|
82
|
+
workspace_id: str
|
|
83
|
+
owner: Owner
|
|
84
|
+
duplicated_template_id: str | None
|
|
85
|
+
request_id: NotRequired[str]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class IntrospectTokenResponse(TypedDict):
|
|
89
|
+
"""Response from introspecting a token."""
|
|
90
|
+
|
|
91
|
+
active: bool
|
|
92
|
+
scope: NotRequired[str]
|
|
93
|
+
iat: NotRequired[int]
|
|
94
|
+
request_id: NotRequired[str]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class RevokeTokenResponse(TypedDict):
|
|
98
|
+
"""Response from revoking a token."""
|
|
99
|
+
|
|
100
|
+
request_id: NotRequired[str]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Rich text types
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RichTextItemResponse(TypedDict):
|
|
107
|
+
"""Rich text item."""
|
|
108
|
+
|
|
109
|
+
type: str
|
|
110
|
+
plain_text: str
|
|
111
|
+
href: str | None
|
|
112
|
+
annotations: dict[str, Any]
|
|
113
|
+
text: NotRequired[dict[str, Any]]
|
|
114
|
+
mention: NotRequired[dict[str, Any]]
|
|
115
|
+
equation: NotRequired[dict[str, Any]]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Parent types
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class PageParent(TypedDict):
|
|
122
|
+
"""Page parent."""
|
|
123
|
+
|
|
124
|
+
type: Literal["page_id"]
|
|
125
|
+
page_id: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DatabaseParent(TypedDict):
|
|
129
|
+
"""Database parent."""
|
|
130
|
+
|
|
131
|
+
type: Literal["database_id"]
|
|
132
|
+
database_id: str
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class WorkspaceParent(TypedDict):
|
|
136
|
+
"""Workspace parent."""
|
|
137
|
+
|
|
138
|
+
type: Literal["workspace"]
|
|
139
|
+
workspace: Literal[True]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class BlockParent(TypedDict):
|
|
143
|
+
"""Block parent."""
|
|
144
|
+
|
|
145
|
+
type: Literal["block_id"]
|
|
146
|
+
block_id: str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
Parent = PageParent | DatabaseParent | WorkspaceParent | BlockParent
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Page types
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Page(TypedDict):
|
|
156
|
+
"""Page object response."""
|
|
157
|
+
|
|
158
|
+
object: Literal["page"]
|
|
159
|
+
id: str
|
|
160
|
+
created_time: str
|
|
161
|
+
last_edited_time: str
|
|
162
|
+
created_by: PartialUser
|
|
163
|
+
last_edited_by: PartialUser
|
|
164
|
+
archived: bool
|
|
165
|
+
in_trash: bool
|
|
166
|
+
is_locked: bool
|
|
167
|
+
url: str
|
|
168
|
+
public_url: str | None
|
|
169
|
+
parent: Parent
|
|
170
|
+
properties: dict[str, Any]
|
|
171
|
+
icon: dict[str, Any] | None
|
|
172
|
+
cover: dict[str, Any] | None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PartialPage(TypedDict):
|
|
176
|
+
"""Partial page object."""
|
|
177
|
+
|
|
178
|
+
object: Literal["page"]
|
|
179
|
+
id: str
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Database types
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Database(TypedDict):
|
|
186
|
+
"""Database object response."""
|
|
187
|
+
|
|
188
|
+
object: Literal["database"]
|
|
189
|
+
id: str
|
|
190
|
+
title: list[RichTextItemResponse]
|
|
191
|
+
description: list[RichTextItemResponse]
|
|
192
|
+
parent: Parent
|
|
193
|
+
is_inline: bool
|
|
194
|
+
in_trash: bool
|
|
195
|
+
is_locked: bool
|
|
196
|
+
created_time: str
|
|
197
|
+
last_edited_time: str
|
|
198
|
+
icon: dict[str, Any] | None
|
|
199
|
+
cover: dict[str, Any] | None
|
|
200
|
+
properties: dict[str, Any]
|
|
201
|
+
url: str
|
|
202
|
+
public_url: str | None
|
|
203
|
+
archived: bool
|
|
204
|
+
created_by: PartialUser
|
|
205
|
+
last_edited_by: PartialUser
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PartialDatabase(TypedDict):
|
|
209
|
+
"""Partial database object."""
|
|
210
|
+
|
|
211
|
+
object: Literal["database"]
|
|
212
|
+
id: str
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Block types
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Block(TypedDict):
|
|
219
|
+
"""Block object response."""
|
|
220
|
+
|
|
221
|
+
object: Literal["block"]
|
|
222
|
+
id: str
|
|
223
|
+
parent: Parent
|
|
224
|
+
type: str
|
|
225
|
+
created_time: str
|
|
226
|
+
last_edited_time: str
|
|
227
|
+
created_by: PartialUser
|
|
228
|
+
last_edited_by: PartialUser
|
|
229
|
+
has_children: bool
|
|
230
|
+
archived: bool
|
|
231
|
+
in_trash: bool
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PartialBlock(TypedDict):
|
|
235
|
+
"""Partial block object."""
|
|
236
|
+
|
|
237
|
+
object: Literal["block"]
|
|
238
|
+
id: str
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# List response types
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class UserListResponse(TypedDict):
|
|
245
|
+
"""Response from listing users."""
|
|
246
|
+
|
|
247
|
+
object: Literal["list"]
|
|
248
|
+
type: Literal["user"]
|
|
249
|
+
results: list[User]
|
|
250
|
+
next_cursor: str | None
|
|
251
|
+
has_more: bool
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class PageListResponse(TypedDict):
|
|
255
|
+
"""Response from querying a database."""
|
|
256
|
+
|
|
257
|
+
object: Literal["list"]
|
|
258
|
+
type: Literal["page_or_data_source"]
|
|
259
|
+
results: list[Page | PartialPage]
|
|
260
|
+
next_cursor: str | None
|
|
261
|
+
has_more: bool
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class BlockListResponse(TypedDict):
|
|
265
|
+
"""Response from listing block children."""
|
|
266
|
+
|
|
267
|
+
object: Literal["list"]
|
|
268
|
+
type: Literal["block"]
|
|
269
|
+
results: list[Block | PartialBlock]
|
|
270
|
+
next_cursor: str | None
|
|
271
|
+
has_more: bool
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class SearchResponse(TypedDict):
|
|
275
|
+
"""Response from search."""
|
|
276
|
+
|
|
277
|
+
object: Literal["list"]
|
|
278
|
+
type: Literal["page_or_data_source"]
|
|
279
|
+
results: list[Page | PartialPage | Database | PartialDatabase]
|
|
280
|
+
next_cursor: str | None
|
|
281
|
+
has_more: bool
|
|
@@ -160,12 +160,34 @@ class PGInsertQuery(PGQuery):
|
|
|
160
160
|
def is_returning(self):
|
|
161
161
|
return self.returning is not None
|
|
162
162
|
|
|
163
|
+
def _get_values_query(self) -> str:
|
|
164
|
+
"""Generate the VALUES clause for the query."""
|
|
165
|
+
_columns = self.columns
|
|
166
|
+
num_cols = len(_columns)
|
|
167
|
+
|
|
168
|
+
if len(self.items) == 1 or not self.is_returning:
|
|
169
|
+
# Single row or no returning: simple placeholders
|
|
170
|
+
return ", ".join(f"${i + 1}" for i in range(num_cols))
|
|
171
|
+
else:
|
|
172
|
+
# Multiple rows with returning: generate all value groups
|
|
173
|
+
value_groups = []
|
|
174
|
+
for row_idx in range(len(self.items)):
|
|
175
|
+
offset = row_idx * num_cols
|
|
176
|
+
group = ", ".join(f"${offset + i + 1}" for i in range(num_cols))
|
|
177
|
+
value_groups.append(f"({group})")
|
|
178
|
+
return ", ".join(value_groups)
|
|
179
|
+
|
|
163
180
|
@property
|
|
164
181
|
def query(self) -> str:
|
|
165
182
|
_columns = self.columns
|
|
166
|
-
_values =
|
|
183
|
+
_values = self._get_values_query()
|
|
167
184
|
|
|
168
|
-
|
|
185
|
+
if len(self.items) > 1 and self.is_returning:
|
|
186
|
+
# For multi-row insert with returning, build full VALUES clause
|
|
187
|
+
_columns_str = ", ".join(_columns)
|
|
188
|
+
query = f"INSERT INTO {self.table} AS t ({_columns_str}) VALUES {_values}"
|
|
189
|
+
else:
|
|
190
|
+
query = _get_insert_query(self.table, _columns, _values)
|
|
169
191
|
|
|
170
192
|
# Conflict
|
|
171
193
|
if self.on_conflict:
|
|
@@ -184,13 +206,14 @@ class PGInsertQuery(PGQuery):
|
|
|
184
206
|
if self.returning is not None:
|
|
185
207
|
if self.returning.returning_ids is None:
|
|
186
208
|
raise ValueError("No returning ids found")
|
|
187
|
-
|
|
188
|
-
query = _get_returning_query(query, self.returning.returning_ids)
|
|
189
|
-
else:
|
|
190
|
-
raise NotImplementedError("Cannot return value when inserting many.")
|
|
209
|
+
query = _get_returning_query(query, self.returning.returning_ids)
|
|
191
210
|
|
|
192
211
|
return query
|
|
193
212
|
|
|
213
|
+
def _get_flat_values(self) -> list:
|
|
214
|
+
"""Get all values as a flat list for multi-row insert with returning."""
|
|
215
|
+
return [val for item_values in self.iter_values() for val in item_values]
|
|
216
|
+
|
|
194
217
|
|
|
195
218
|
def get_update_fields(
|
|
196
219
|
item: dict,
|
|
@@ -341,6 +364,7 @@ async def insert_one(
|
|
|
341
364
|
await query.run(conn)
|
|
342
365
|
|
|
343
366
|
|
|
367
|
+
@overload
|
|
344
368
|
async def insert_many(
|
|
345
369
|
conn: _Connection,
|
|
346
370
|
table: str,
|
|
@@ -349,12 +373,53 @@ async def insert_many(
|
|
|
349
373
|
on_conflict: OnConflict | None = None,
|
|
350
374
|
fill: bool = False,
|
|
351
375
|
quote_columns: bool = False,
|
|
376
|
+
returning: None = None,
|
|
352
377
|
query_callback: QueryCallback[PGInsertQuery] | None = None,
|
|
353
|
-
):
|
|
354
|
-
|
|
378
|
+
) -> None: ...
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@overload
|
|
382
|
+
async def insert_many(
|
|
383
|
+
conn: _Connection,
|
|
384
|
+
table: str,
|
|
385
|
+
items: list[dict],
|
|
386
|
+
*,
|
|
387
|
+
on_conflict: OnConflict | None = None,
|
|
388
|
+
fill: bool = False,
|
|
389
|
+
quote_columns: bool = False,
|
|
390
|
+
returning: str | list[str],
|
|
391
|
+
query_callback: QueryCallback[PGInsertQuery] | None = None,
|
|
392
|
+
) -> list[asyncpg.Record]: ...
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
async def insert_many(
|
|
396
|
+
conn: _Connection,
|
|
397
|
+
table: str,
|
|
398
|
+
items: list[dict],
|
|
399
|
+
*,
|
|
400
|
+
on_conflict: OnConflict | None = None,
|
|
401
|
+
fill: bool = False,
|
|
402
|
+
quote_columns: bool = False,
|
|
403
|
+
returning: str | list[str] | None = None,
|
|
404
|
+
query_callback: QueryCallback[PGInsertQuery] | None = None,
|
|
405
|
+
) -> list[asyncpg.Record] | None:
|
|
406
|
+
returning_values = [returning] if isinstance(returning, str) else returning
|
|
407
|
+
query = insert_pg(
|
|
408
|
+
table=table,
|
|
409
|
+
items=items,
|
|
410
|
+
on_conflict=on_conflict,
|
|
411
|
+
fill=fill,
|
|
412
|
+
quote_columns=quote_columns,
|
|
413
|
+
returning=returning_values,
|
|
414
|
+
)
|
|
355
415
|
if query_callback is not None:
|
|
356
416
|
query_callback(query)
|
|
357
|
-
|
|
417
|
+
|
|
418
|
+
if returning is not None:
|
|
419
|
+
return await conn.fetch(query.query, *query._get_flat_values())
|
|
420
|
+
else:
|
|
421
|
+
await query.run(conn)
|
|
422
|
+
return None
|
|
358
423
|
|
|
359
424
|
|
|
360
425
|
@overload
|
|
@@ -381,22 +446,41 @@ async def insert_returning(
|
|
|
381
446
|
) -> asyncpg.Record | None: ...
|
|
382
447
|
|
|
383
448
|
|
|
449
|
+
@overload
|
|
384
450
|
async def insert_returning(
|
|
385
451
|
conn: _Connection,
|
|
386
452
|
table: str,
|
|
387
|
-
item: dict,
|
|
453
|
+
item: list[dict],
|
|
454
|
+
returning: str | list[str],
|
|
455
|
+
on_conflict: OnConflict | None = None,
|
|
456
|
+
fill: bool = False,
|
|
457
|
+
query_callback: QueryCallback[PGInsertQuery] | None = None,
|
|
458
|
+
) -> list[asyncpg.Record]: ...
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
async def insert_returning(
|
|
462
|
+
conn: _Connection,
|
|
463
|
+
table: str,
|
|
464
|
+
item: dict | list[dict],
|
|
388
465
|
returning: list[str] | str,
|
|
389
466
|
on_conflict: OnConflict | None = None,
|
|
390
467
|
fill: bool = False,
|
|
391
468
|
query_callback: QueryCallback[PGInsertQuery] | None = None,
|
|
392
|
-
) -> asyncpg.Record | Any | None:
|
|
469
|
+
) -> asyncpg.Record | Any | list[asyncpg.Record] | None:
|
|
393
470
|
returning_values = [returning] if isinstance(returning, str) else returning
|
|
394
|
-
|
|
471
|
+
items = item if isinstance(item, list) else [item]
|
|
472
|
+
|
|
473
|
+
query = insert_pg(table=table, items=items, on_conflict=on_conflict, fill=fill, returning=returning_values)
|
|
395
474
|
if query_callback is not None:
|
|
396
475
|
query_callback(query)
|
|
397
|
-
fn = conn.fetchval if len(returning_values) == 1 and returning != "*" else conn.fetchrow
|
|
398
476
|
|
|
399
|
-
|
|
477
|
+
if len(items) > 1:
|
|
478
|
+
# Multi-row insert: use fetch() with flat values
|
|
479
|
+
return await conn.fetch(query.query, *query._get_flat_values())
|
|
480
|
+
else:
|
|
481
|
+
# Single row insert: use fetchval or fetchrow
|
|
482
|
+
fn = conn.fetchval if len(returning_values) == 1 and returning != "*" else conn.fetchrow
|
|
483
|
+
return await fn(query.query, *query.values)
|
|
400
484
|
|
|
401
485
|
|
|
402
486
|
async def fetch_count(conn: _Connection, query: str, *args) -> int:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|