tracktolib 0.62.0__py3-none-any.whl → 0.63.0__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.
- tracktolib/notion/__init__.py +0 -0
- tracktolib/notion/fetch.py +352 -0
- tracktolib/notion/models.py +281 -0
- tracktolib/pg/query.py +16 -4
- {tracktolib-0.62.0.dist-info → tracktolib-0.63.0.dist-info}/METADATA +3 -1
- {tracktolib-0.62.0.dist-info → tracktolib-0.63.0.dist-info}/RECORD +7 -4
- {tracktolib-0.62.0.dist-info → tracktolib-0.63.0.dist-info}/WHEEL +1 -1
|
File without changes
|
|
@@ -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
|
tracktolib/pg/query.py
CHANGED
|
@@ -225,10 +225,14 @@ def get_update_fields(
|
|
|
225
225
|
|
|
226
226
|
@dataclass
|
|
227
227
|
class PGUpdateQuery(PGQuery):
|
|
228
|
-
"""
|
|
229
|
-
|
|
228
|
+
"""
|
|
229
|
+
Postgresql UPDATE query generator
|
|
230
230
|
"""
|
|
231
231
|
|
|
232
|
+
"""
|
|
233
|
+
Value to start the arguments from:
|
|
234
|
+
For instance, with a value of 10, the first argument will be $11
|
|
235
|
+
"""
|
|
232
236
|
start_from: int | None = None
|
|
233
237
|
"""Keys to use for the WHERE clause. Theses fields will not be updated"""
|
|
234
238
|
where_keys: list[str] | None = None
|
|
@@ -239,6 +243,8 @@ class PGUpdateQuery(PGQuery):
|
|
|
239
243
|
return_keys: bool = False
|
|
240
244
|
"""Values to update using merge (like {}::jsonb || {}::jsonb)"""
|
|
241
245
|
merge_keys: list[str] | None = None
|
|
246
|
+
"""If True, the query is for many items and values will be a list of tuples"""
|
|
247
|
+
is_many: bool = False
|
|
242
248
|
|
|
243
249
|
_update_fields: str | None = field(init=False, default=None)
|
|
244
250
|
_values: list | None = field(init=False, default=None)
|
|
@@ -264,7 +270,7 @@ class PGUpdateQuery(PGQuery):
|
|
|
264
270
|
def values(self):
|
|
265
271
|
if not self._values:
|
|
266
272
|
raise ValueError("No values found")
|
|
267
|
-
if len(self.items) == 1:
|
|
273
|
+
if len(self.items) == 1 and not self.is_many:
|
|
268
274
|
return self._values
|
|
269
275
|
_where_keys = self.where_keys or []
|
|
270
276
|
_keys_not_where = [k for k in self.keys if k not in _where_keys]
|
|
@@ -516,7 +522,13 @@ async def update_many(
|
|
|
516
522
|
query_callback: QueryCallback[PGUpdateQuery] | None = None,
|
|
517
523
|
):
|
|
518
524
|
query = PGUpdateQuery(
|
|
519
|
-
table=table,
|
|
525
|
+
table=table,
|
|
526
|
+
items=items,
|
|
527
|
+
start_from=start_from,
|
|
528
|
+
where_keys=keys,
|
|
529
|
+
where=where,
|
|
530
|
+
merge_keys=merge_keys,
|
|
531
|
+
is_many=True,
|
|
520
532
|
)
|
|
521
533
|
if query_callback is not None:
|
|
522
534
|
query_callback(query)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tracktolib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.63.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
|
|
@@ -2,8 +2,11 @@ tracktolib/__init__.py,sha256=Q9d6h2lNjcYzxvfJ3zlNcpiP_Ak0T3TBPWINzZNrhu0,173
|
|
|
2
2
|
tracktolib/api.py,sha256=ZLMgjH3Y8r3MpXc8m3IuZbzTj3fgrZKZORtSVgbuP-M,10221
|
|
3
3
|
tracktolib/http_utils.py,sha256=c10JGmHaBw3VSDMYhz2dvVw2lo4PUAq1xMub74I7xDc,2625
|
|
4
4
|
tracktolib/logs.py,sha256=D2hx6urXl5l4PBGP8mCpcT4GX7tJeFfNY-7oBfHczBU,2191
|
|
5
|
+
tracktolib/notion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
tracktolib/notion/fetch.py,sha256=zIhFo9T18eS75l0wZN_WaXD51OUxTsgvFuvqk3R87o4,9974
|
|
7
|
+
tracktolib/notion/models.py,sha256=D-px7Ht2xoXKKZICuEkZhRwVBvVCaKCvhpzPtVVYawI,5390
|
|
5
8
|
tracktolib/pg/__init__.py,sha256=Ul_hgwvTXZvQBt7sHKi4ZI-0DDpnXmoFtmVkGRy-1J0,366
|
|
6
|
-
tracktolib/pg/query.py,sha256=
|
|
9
|
+
tracktolib/pg/query.py,sha256=_wL9MQU_z8Sk0ZOYGVE0TjUbwBZ1OJJuEq2jlmWoeeM,16693
|
|
7
10
|
tracktolib/pg/utils.py,sha256=ygQn63EBDaEGB0p7P2ibellO2mv-StafanpXKcCUiZU,6324
|
|
8
11
|
tracktolib/pg_sync.py,sha256=MKDaV7dYsRy59Y0EE5RGZL0DlZ-RUdBeaN9eSBwiQJg,6718
|
|
9
12
|
tracktolib/pg_utils.py,sha256=ArYNdf9qsdYdzGEWmev8tZpyx8_1jaGGdkfYkauM7UM,2582
|
|
@@ -12,6 +15,6 @@ tracktolib/s3/minio.py,sha256=wMEjkSes9Fp39fD17IctALpD6zB2xwDRQEmO7Vzan3g,1387
|
|
|
12
15
|
tracktolib/s3/s3.py,sha256=0HbSAPoaup5-W4LK54zRCjrQ5mr8OWR-N9WjW99Q4aw,5937
|
|
13
16
|
tracktolib/tests.py,sha256=gKE--epQjgMZGXc5ydbl4zjOdmwztJS42UMV0p4hXEA,399
|
|
14
17
|
tracktolib/utils.py,sha256=ysTBF9V35fVXQVBPk0kfE_84SGRxzrayqmg9RbtoJq4,5761
|
|
15
|
-
tracktolib-0.
|
|
16
|
-
tracktolib-0.
|
|
17
|
-
tracktolib-0.
|
|
18
|
+
tracktolib-0.63.0.dist-info/WHEEL,sha256=z-mOpxbJHqy3cq6SvUThBZdaLGFZzdZPtgWLcP2NKjQ,79
|
|
19
|
+
tracktolib-0.63.0.dist-info/METADATA,sha256=q3HcfLHVoybHIDc4Bgayt3kYd5v1spd62OCGyxjCfZw,3128
|
|
20
|
+
tracktolib-0.63.0.dist-info/RECORD,,
|