python-dropbox-api 0.1.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.
- python_dropbox_api-0.1.0/PKG-INFO +16 -0
- python_dropbox_api-0.1.0/README.md +3 -0
- python_dropbox_api-0.1.0/pyproject.toml +22 -0
- python_dropbox_api-0.1.0/python_dropbox_api/__init__.py +22 -0
- python_dropbox_api-0.1.0/python_dropbox_api/auth.py +17 -0
- python_dropbox_api-0.1.0/python_dropbox_api/client.py +572 -0
- python_dropbox_api-0.1.0/python_dropbox_api/exceptions.py +16 -0
- python_dropbox_api-0.1.0/python_dropbox_api/models.py +78 -0
- python_dropbox_api-0.1.0/python_dropbox_api/py.typed +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-dropbox-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Brandon Rothweiler
|
|
6
|
+
Author-email: 2292715+bdr99@users.noreply.github.com
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Dist: aiohttp (>=3.13.0,<4.0.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# python-dropbox-api
|
|
15
|
+
|
|
16
|
+
This is a lightweight wrapper for the Dropbox API intended for use in Home Assistant.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-dropbox-api"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Brandon Rothweiler",email = "2292715+bdr99@users.noreply.github.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"aiohttp (>=3.13.0,<4.0.0)"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.poetry]
|
|
15
|
+
packages = [
|
|
16
|
+
{include = "python_dropbox_api"},
|
|
17
|
+
{include = "python_dropbox_api/py.typed"},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .auth import Auth
|
|
2
|
+
from .client import DropboxAPIClient
|
|
3
|
+
from .exceptions import DropboxAuthException, DropboxFileOrFolderNotFoundException
|
|
4
|
+
from .models import (
|
|
5
|
+
AccountInfo,
|
|
6
|
+
PropertyField,
|
|
7
|
+
PropertyFieldValue,
|
|
8
|
+
PropertyGroup,
|
|
9
|
+
PropertyTemplate,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AccountInfo",
|
|
14
|
+
"Auth",
|
|
15
|
+
"DropboxAPIClient",
|
|
16
|
+
"DropboxAuthException",
|
|
17
|
+
"DropboxFileOrFolderNotFoundException",
|
|
18
|
+
"PropertyField",
|
|
19
|
+
"PropertyFieldValue",
|
|
20
|
+
"PropertyGroup",
|
|
21
|
+
"PropertyTemplate",
|
|
22
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from aiohttp import ClientSession
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Auth(ABC):
|
|
8
|
+
"""Class to get a valid access token."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, websession: ClientSession) -> None:
|
|
11
|
+
"""Initialize the auth."""
|
|
12
|
+
self._logger = logging.getLogger(__name__)
|
|
13
|
+
self.websession = websession
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
async def async_get_access_token(self) -> str:
|
|
17
|
+
"""Return a valid access token."""
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from aiohttp import ClientResponse
|
|
5
|
+
|
|
6
|
+
from .auth import Auth
|
|
7
|
+
from .exceptions import DropboxAuthException, DropboxFileOrFolderNotFoundException
|
|
8
|
+
from .models import (
|
|
9
|
+
AccountInfo,
|
|
10
|
+
FileOrFolderInfo,
|
|
11
|
+
PropertyField,
|
|
12
|
+
PropertyFieldValue,
|
|
13
|
+
PropertyGroup,
|
|
14
|
+
PropertyTemplate,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
DROPBOX_API_BASE_URL = "https://api.dropboxapi.com"
|
|
18
|
+
CONTENT_API_BASE_URL = "https://content.dropboxapi.com"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_property_groups(property_groups: list[dict]) -> list[PropertyGroup]:
|
|
22
|
+
"""Parse the property groups from the response."""
|
|
23
|
+
return [
|
|
24
|
+
PropertyGroup(
|
|
25
|
+
template_id=group["template_id"],
|
|
26
|
+
fields=[
|
|
27
|
+
PropertyFieldValue(name=field["name"], value=field["value"])
|
|
28
|
+
for field in group["fields"]
|
|
29
|
+
],
|
|
30
|
+
)
|
|
31
|
+
for group in property_groups
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def handle_common_errors(response: ClientResponse) -> None:
|
|
36
|
+
"""Handle common errors."""
|
|
37
|
+
response_json = await response.json()
|
|
38
|
+
|
|
39
|
+
error_summary = response_json.get("error_summary")
|
|
40
|
+
|
|
41
|
+
if error_summary is not None:
|
|
42
|
+
if error_summary.startswith("invalid_access_token/"):
|
|
43
|
+
raise DropboxAuthException("Unauthorized")
|
|
44
|
+
if error_summary.startswith("path/not_found/"):
|
|
45
|
+
raise DropboxFileOrFolderNotFoundException("File or folder not found")
|
|
46
|
+
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DropboxAPIClient:
|
|
51
|
+
"""Lightweight Dropbox API client."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, auth: Auth) -> None:
|
|
54
|
+
"""Initialize the API client."""
|
|
55
|
+
self._websession = auth.websession
|
|
56
|
+
self._auth = auth
|
|
57
|
+
|
|
58
|
+
async def get_account_info(self) -> AccountInfo:
|
|
59
|
+
"""Get information about the current account.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
AccountInfo containing account_id, email, and display_name.
|
|
63
|
+
"""
|
|
64
|
+
access_token = await self._auth.async_get_access_token()
|
|
65
|
+
|
|
66
|
+
headers = {
|
|
67
|
+
"Authorization": f"Bearer {access_token}",
|
|
68
|
+
"Content-Type": "",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
url = f"{DROPBOX_API_BASE_URL}/2/users/get_current_account"
|
|
72
|
+
|
|
73
|
+
async with self._websession.post(url, headers=headers) as response:
|
|
74
|
+
await handle_common_errors(response)
|
|
75
|
+
|
|
76
|
+
response_json = await response.json()
|
|
77
|
+
|
|
78
|
+
return AccountInfo(
|
|
79
|
+
account_id=response_json["account_id"],
|
|
80
|
+
email=response_json["email"],
|
|
81
|
+
display_name=response_json["name"]["display_name"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def list_folder(
|
|
85
|
+
self, path: str, include_property_groups: list[str] | None = None
|
|
86
|
+
) -> list[FileOrFolderInfo]:
|
|
87
|
+
"""List the contents of a folder with pagination support."""
|
|
88
|
+
|
|
89
|
+
access_token = await self._auth.async_get_access_token()
|
|
90
|
+
|
|
91
|
+
headers = {
|
|
92
|
+
"Authorization": f"Bearer {access_token}",
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Initial request data
|
|
97
|
+
data = {
|
|
98
|
+
"include_deleted": False,
|
|
99
|
+
"include_has_explicit_shared_members": False,
|
|
100
|
+
"include_media_info": False,
|
|
101
|
+
"include_mounted_folders": True,
|
|
102
|
+
"include_non_downloadable_files": False,
|
|
103
|
+
"path": path,
|
|
104
|
+
"recursive": False,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if include_property_groups is not None:
|
|
108
|
+
data["include_property_groups"] = {
|
|
109
|
+
".tag": "filter_some",
|
|
110
|
+
"filter_some": include_property_groups,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
all_entries = []
|
|
114
|
+
cursor = None
|
|
115
|
+
has_more = True
|
|
116
|
+
|
|
117
|
+
while has_more:
|
|
118
|
+
# Use list_folder endpoint for first request, list_folder/continue for subsequent ones
|
|
119
|
+
if cursor is None:
|
|
120
|
+
url = f"{DROPBOX_API_BASE_URL}/2/files/list_folder"
|
|
121
|
+
request_data = data
|
|
122
|
+
else:
|
|
123
|
+
url = f"{DROPBOX_API_BASE_URL}/2/files/list_folder/continue"
|
|
124
|
+
request_data = {"cursor": cursor}
|
|
125
|
+
|
|
126
|
+
async with self._websession.post(
|
|
127
|
+
url, headers=headers, json=request_data
|
|
128
|
+
) as response:
|
|
129
|
+
await handle_common_errors(response)
|
|
130
|
+
|
|
131
|
+
response_json = await response.json()
|
|
132
|
+
|
|
133
|
+
# Add entries from this page to our collection
|
|
134
|
+
page_entries = [
|
|
135
|
+
FileOrFolderInfo(
|
|
136
|
+
is_folder=file[".tag"] == "folder",
|
|
137
|
+
name=file["name"],
|
|
138
|
+
size=file["size"] if file[".tag"] == "file" else None,
|
|
139
|
+
property_groups=parse_property_groups(file["property_groups"])
|
|
140
|
+
if "property_groups" in file
|
|
141
|
+
else None,
|
|
142
|
+
)
|
|
143
|
+
for file in response_json["entries"]
|
|
144
|
+
]
|
|
145
|
+
all_entries.extend(page_entries)
|
|
146
|
+
|
|
147
|
+
# Check if there are more pages
|
|
148
|
+
has_more = response_json.get("has_more", False)
|
|
149
|
+
cursor = response_json.get("cursor")
|
|
150
|
+
|
|
151
|
+
return all_entries
|
|
152
|
+
|
|
153
|
+
async def get_metadata(
|
|
154
|
+
self, path: str, include_property_groups: list[str] | None = None
|
|
155
|
+
) -> FileOrFolderInfo:
|
|
156
|
+
"""Get metadata for a file or folder.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
path: The Dropbox path to get metadata for.
|
|
160
|
+
include_property_groups: Optional list of property group template IDs to include.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
FileOrFolderInfo containing metadata for the file or folder.
|
|
164
|
+
"""
|
|
165
|
+
access_token = await self._auth.async_get_access_token()
|
|
166
|
+
|
|
167
|
+
headers = {
|
|
168
|
+
"Authorization": f"Bearer {access_token}",
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
data = {
|
|
173
|
+
"include_deleted": False,
|
|
174
|
+
"include_has_explicit_shared_members": False,
|
|
175
|
+
"include_media_info": False,
|
|
176
|
+
"path": path,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if include_property_groups is not None:
|
|
180
|
+
data["include_property_groups"] = {
|
|
181
|
+
".tag": "filter_some",
|
|
182
|
+
"filter_some": include_property_groups,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
url = f"{DROPBOX_API_BASE_URL}/2/files/get_metadata"
|
|
186
|
+
|
|
187
|
+
async with self._websession.post(url, headers=headers, json=data) as response:
|
|
188
|
+
await handle_common_errors(response)
|
|
189
|
+
|
|
190
|
+
response_json = await response.json()
|
|
191
|
+
|
|
192
|
+
return FileOrFolderInfo(
|
|
193
|
+
is_folder=response_json[".tag"] == "folder",
|
|
194
|
+
name=response_json["name"],
|
|
195
|
+
size=response_json["size"] if response_json[".tag"] == "file" else None,
|
|
196
|
+
property_groups=parse_property_groups(response_json["property_groups"])
|
|
197
|
+
if "property_groups" in response_json
|
|
198
|
+
else None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def upload_file(
|
|
202
|
+
self,
|
|
203
|
+
path: str,
|
|
204
|
+
file_stream: AsyncIterator[bytes],
|
|
205
|
+
property_groups: list[PropertyGroup] | None = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Upload a file to Dropbox using chunked upload sessions.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
path: The Dropbox path where the file should be uploaded.
|
|
211
|
+
file_stream: An async iterator of bytes to upload.
|
|
212
|
+
property_groups: Optional list of property groups to attach to the uploaded file.
|
|
213
|
+
"""
|
|
214
|
+
CHUNK_SIZE = 16 * 1024 * 1024 # 16MB chunks
|
|
215
|
+
|
|
216
|
+
# Always use chunked upload approach
|
|
217
|
+
await self._upload_file_chunked(
|
|
218
|
+
CONTENT_API_BASE_URL, path, file_stream, CHUNK_SIZE, property_groups
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async def _upload_file_chunked(
|
|
222
|
+
self,
|
|
223
|
+
content_api_url: str,
|
|
224
|
+
path: str,
|
|
225
|
+
file_stream: AsyncIterator[bytes],
|
|
226
|
+
chunk_size: int,
|
|
227
|
+
property_groups: list[PropertyGroup] | None = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Upload a file using chunked upload sessions, streaming chunks without loading entire file into memory."""
|
|
230
|
+
session_id = None
|
|
231
|
+
offset = 0
|
|
232
|
+
is_first_chunk = True
|
|
233
|
+
|
|
234
|
+
# Buffer to accumulate chunks until we have enough for a full chunk
|
|
235
|
+
buffer = b""
|
|
236
|
+
|
|
237
|
+
async for data_chunk in file_stream:
|
|
238
|
+
buffer += data_chunk
|
|
239
|
+
|
|
240
|
+
# Process full chunks from the buffer
|
|
241
|
+
while len(buffer) >= chunk_size:
|
|
242
|
+
chunk_data = buffer[:chunk_size]
|
|
243
|
+
buffer = buffer[chunk_size:]
|
|
244
|
+
|
|
245
|
+
if is_first_chunk:
|
|
246
|
+
# Start upload session with first chunk
|
|
247
|
+
session_id = await self._start_upload_session(
|
|
248
|
+
content_api_url, chunk_data
|
|
249
|
+
)
|
|
250
|
+
is_first_chunk = False
|
|
251
|
+
else:
|
|
252
|
+
# Append chunk to existing session
|
|
253
|
+
await self._append_to_upload_session(
|
|
254
|
+
content_api_url, session_id, offset, chunk_data
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
offset += len(chunk_data)
|
|
258
|
+
|
|
259
|
+
# Handle remaining data in buffer (final partial chunk)
|
|
260
|
+
if buffer:
|
|
261
|
+
if is_first_chunk:
|
|
262
|
+
# If this is the only chunk, start session with it
|
|
263
|
+
session_id = await self._start_upload_session(content_api_url, buffer)
|
|
264
|
+
else:
|
|
265
|
+
# Append final chunk
|
|
266
|
+
await self._append_to_upload_session(
|
|
267
|
+
content_api_url, session_id, offset, buffer
|
|
268
|
+
)
|
|
269
|
+
offset += len(buffer)
|
|
270
|
+
|
|
271
|
+
# Finish upload session
|
|
272
|
+
await self._finish_upload_session(
|
|
273
|
+
content_api_url, session_id, offset, path, property_groups
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
async def _start_upload_session(
|
|
277
|
+
self, content_api_url: str, chunk_data: bytes
|
|
278
|
+
) -> str:
|
|
279
|
+
"""Start an upload session with the first chunk."""
|
|
280
|
+
access_token = await self._auth.async_get_access_token()
|
|
281
|
+
|
|
282
|
+
headers = {
|
|
283
|
+
"Authorization": f"Bearer {access_token}",
|
|
284
|
+
"Dropbox-API-Arg": json.dumps({"close": False}),
|
|
285
|
+
"Content-Type": "application/octet-stream",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async with self._websession.post(
|
|
289
|
+
f"{content_api_url}/2/files/upload_session/start",
|
|
290
|
+
headers=headers,
|
|
291
|
+
data=chunk_data,
|
|
292
|
+
) as response:
|
|
293
|
+
await handle_common_errors(response)
|
|
294
|
+
|
|
295
|
+
response_json = await response.json()
|
|
296
|
+
return response_json["session_id"]
|
|
297
|
+
|
|
298
|
+
async def _append_to_upload_session(
|
|
299
|
+
self, content_api_url: str, session_id: str, offset: int, chunk_data: bytes
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Append a chunk to an existing upload session."""
|
|
302
|
+
access_token = await self._auth.async_get_access_token()
|
|
303
|
+
|
|
304
|
+
headers = {
|
|
305
|
+
"Authorization": f"Bearer {access_token}",
|
|
306
|
+
"Dropbox-API-Arg": json.dumps(
|
|
307
|
+
{
|
|
308
|
+
"close": False,
|
|
309
|
+
"cursor": {
|
|
310
|
+
"session_id": session_id,
|
|
311
|
+
"offset": offset,
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
),
|
|
315
|
+
"Content-Type": "application/octet-stream",
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async with self._websession.post(
|
|
319
|
+
f"{content_api_url}/2/files/upload_session/append_v2",
|
|
320
|
+
headers=headers,
|
|
321
|
+
data=chunk_data,
|
|
322
|
+
) as response:
|
|
323
|
+
await handle_common_errors(response)
|
|
324
|
+
|
|
325
|
+
async def _finish_upload_session(
|
|
326
|
+
self,
|
|
327
|
+
content_api_url: str,
|
|
328
|
+
session_id: str,
|
|
329
|
+
total_size: int,
|
|
330
|
+
path: str,
|
|
331
|
+
property_groups: list[PropertyGroup] | None = None,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Finish an upload session."""
|
|
334
|
+
# Build commit data
|
|
335
|
+
commit_data = {
|
|
336
|
+
"path": path,
|
|
337
|
+
"mode": "add",
|
|
338
|
+
"autorename": True,
|
|
339
|
+
"mute": True,
|
|
340
|
+
"strict_conflict": True,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# Add property groups if provided
|
|
344
|
+
if property_groups:
|
|
345
|
+
commit_data["property_groups"] = [
|
|
346
|
+
{
|
|
347
|
+
"template_id": group.template_id,
|
|
348
|
+
"fields": [
|
|
349
|
+
{"name": field.name, "value": field.value}
|
|
350
|
+
for field in group.fields
|
|
351
|
+
],
|
|
352
|
+
}
|
|
353
|
+
for group in property_groups
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
access_token = await self._auth.async_get_access_token()
|
|
357
|
+
|
|
358
|
+
headers = {
|
|
359
|
+
"Authorization": f"Bearer {access_token}",
|
|
360
|
+
"Dropbox-API-Arg": json.dumps(
|
|
361
|
+
{
|
|
362
|
+
"commit": commit_data,
|
|
363
|
+
"cursor": {
|
|
364
|
+
"session_id": session_id,
|
|
365
|
+
"offset": total_size,
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
),
|
|
369
|
+
"Content-Type": "application/octet-stream",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# Send empty data for finish request
|
|
373
|
+
async with self._websession.post(
|
|
374
|
+
f"{content_api_url}/2/files/upload_session/finish",
|
|
375
|
+
headers=headers,
|
|
376
|
+
data=b"",
|
|
377
|
+
) as response:
|
|
378
|
+
await handle_common_errors(response)
|
|
379
|
+
|
|
380
|
+
async def download_file(self, path: str) -> AsyncIterator[bytes]:
|
|
381
|
+
"""Download a file from Dropbox as an async byte stream.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
path: The Dropbox path to the file to download.
|
|
385
|
+
|
|
386
|
+
Yields:
|
|
387
|
+
Chunks of file content as bytes.
|
|
388
|
+
"""
|
|
389
|
+
access_token = await self._auth.async_get_access_token()
|
|
390
|
+
|
|
391
|
+
headers = {
|
|
392
|
+
"Authorization": f"Bearer {access_token}",
|
|
393
|
+
"Dropbox-API-Arg": json.dumps({"path": path}),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
url = f"{CONTENT_API_BASE_URL}/2/files/download"
|
|
397
|
+
chunk_size = 1024 * 1024 # 1MB
|
|
398
|
+
|
|
399
|
+
async with self._websession.post(url, headers=headers) as response:
|
|
400
|
+
response.raise_for_status()
|
|
401
|
+
async for chunk in response.content.iter_chunked(chunk_size):
|
|
402
|
+
if chunk:
|
|
403
|
+
yield chunk
|
|
404
|
+
|
|
405
|
+
async def create_folder(self, path: str) -> FileOrFolderInfo:
|
|
406
|
+
"""Create a folder in Dropbox.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
path: The Dropbox path where the folder should be created.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
FileOrFolderInfo containing metadata for the created folder.
|
|
413
|
+
"""
|
|
414
|
+
access_token = await self._auth.async_get_access_token()
|
|
415
|
+
|
|
416
|
+
headers = {
|
|
417
|
+
"Authorization": f"Bearer {access_token}",
|
|
418
|
+
"Content-Type": "application/json",
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
url = f"{DROPBOX_API_BASE_URL}/2/files/create_folder_v2"
|
|
422
|
+
payload = {
|
|
423
|
+
"path": path,
|
|
424
|
+
"autorename": False,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async with self._websession.post(
|
|
428
|
+
url, headers=headers, json=payload
|
|
429
|
+
) as response:
|
|
430
|
+
await handle_common_errors(response)
|
|
431
|
+
|
|
432
|
+
async def delete_file(self, path: str) -> None:
|
|
433
|
+
"""Delete a file (moves to Dropbox trash).
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
path: The Dropbox path of the file to delete.
|
|
437
|
+
"""
|
|
438
|
+
access_token = await self._auth.async_get_access_token()
|
|
439
|
+
|
|
440
|
+
headers = {
|
|
441
|
+
"Authorization": f"Bearer {access_token}",
|
|
442
|
+
"Content-Type": "application/json",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
url = f"{DROPBOX_API_BASE_URL}/2/files/delete_v2"
|
|
446
|
+
payload = {"path": path}
|
|
447
|
+
|
|
448
|
+
async with self._websession.post(
|
|
449
|
+
url, headers=headers, json=payload
|
|
450
|
+
) as response:
|
|
451
|
+
await handle_common_errors(response)
|
|
452
|
+
|
|
453
|
+
async def list_property_templates(self) -> list[str]:
|
|
454
|
+
"""List property group templates for the current user.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
A list of template IDs for property group templates.
|
|
458
|
+
"""
|
|
459
|
+
access_token = await self._auth.async_get_access_token()
|
|
460
|
+
|
|
461
|
+
headers = {
|
|
462
|
+
"Authorization": f"Bearer {access_token}",
|
|
463
|
+
"Content-Type": "",
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
url = f"{DROPBOX_API_BASE_URL}/2/file_properties/templates/list_for_user"
|
|
467
|
+
|
|
468
|
+
async with self._websession.post(url, headers=headers) as response:
|
|
469
|
+
await handle_common_errors(response)
|
|
470
|
+
|
|
471
|
+
response_json = await response.json()
|
|
472
|
+
return response_json["template_ids"]
|
|
473
|
+
|
|
474
|
+
async def add_property_template(self, template: PropertyTemplate) -> str:
|
|
475
|
+
"""Add a property group template for the current user.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
template: The property template to add.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
A string containing the template ID.
|
|
482
|
+
"""
|
|
483
|
+
access_token = await self._auth.async_get_access_token()
|
|
484
|
+
|
|
485
|
+
headers = {
|
|
486
|
+
"Authorization": f"Bearer {access_token}",
|
|
487
|
+
"Content-Type": "application/json",
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# Convert the template to the format expected by the API
|
|
491
|
+
data = {
|
|
492
|
+
"name": template.name,
|
|
493
|
+
"description": template.description,
|
|
494
|
+
"fields": [
|
|
495
|
+
{
|
|
496
|
+
"name": field.name,
|
|
497
|
+
"description": field.description,
|
|
498
|
+
"type": field.type,
|
|
499
|
+
}
|
|
500
|
+
for field in template.fields
|
|
501
|
+
],
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
url = f"{DROPBOX_API_BASE_URL}/2/file_properties/templates/add_for_user"
|
|
505
|
+
|
|
506
|
+
async with self._websession.post(url, headers=headers, json=data) as response:
|
|
507
|
+
await handle_common_errors(response)
|
|
508
|
+
|
|
509
|
+
response_json = await response.json()
|
|
510
|
+
return response_json["template_id"]
|
|
511
|
+
|
|
512
|
+
async def remove_property_template(self, template_id: str) -> None:
|
|
513
|
+
"""Remove a property group template for the current user.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
template_id: The ID of the property template to remove.
|
|
517
|
+
"""
|
|
518
|
+
access_token = await self._auth.async_get_access_token()
|
|
519
|
+
|
|
520
|
+
headers = {
|
|
521
|
+
"Authorization": f"Bearer {access_token}",
|
|
522
|
+
"Content-Type": "application/json",
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
data = {"template_id": template_id}
|
|
526
|
+
|
|
527
|
+
url = f"{DROPBOX_API_BASE_URL}/2/file_properties/templates/remove_for_user"
|
|
528
|
+
|
|
529
|
+
async with self._websession.post(url, headers=headers, json=data) as response:
|
|
530
|
+
await handle_common_errors(response)
|
|
531
|
+
|
|
532
|
+
async def get_property_template(self, template_id: str) -> PropertyTemplate:
|
|
533
|
+
"""Get a property group template for the current user.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
template_id: The ID of the property template to get.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
The PropertyTemplate.
|
|
540
|
+
"""
|
|
541
|
+
access_token = await self._auth.async_get_access_token()
|
|
542
|
+
|
|
543
|
+
headers = {
|
|
544
|
+
"Authorization": f"Bearer {access_token}",
|
|
545
|
+
"Content-Type": "application/json",
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
data = {"template_id": template_id}
|
|
549
|
+
|
|
550
|
+
url = f"{DROPBOX_API_BASE_URL}/2/file_properties/templates/get_for_user"
|
|
551
|
+
|
|
552
|
+
async with self._websession.post(url, headers=headers, json=data) as response:
|
|
553
|
+
await handle_common_errors(response)
|
|
554
|
+
|
|
555
|
+
response_json = await response.json()
|
|
556
|
+
|
|
557
|
+
fields = [
|
|
558
|
+
PropertyField(
|
|
559
|
+
name=field["name"],
|
|
560
|
+
description=field["description"],
|
|
561
|
+
type=field["type"][".tag"]
|
|
562
|
+
if isinstance(field["type"], dict)
|
|
563
|
+
else field["type"],
|
|
564
|
+
)
|
|
565
|
+
for field in response_json["fields"]
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
return PropertyTemplate(
|
|
569
|
+
name=response_json["name"],
|
|
570
|
+
description=response_json["description"],
|
|
571
|
+
fields=fields,
|
|
572
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class DropboxFileOrFolderNotFoundException(Exception):
|
|
2
|
+
"""Raised when a file or folder is not found."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, status):
|
|
5
|
+
"""Initialize exception."""
|
|
6
|
+
super().__init__(status)
|
|
7
|
+
self.status = status
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DropboxAuthException(Exception):
|
|
11
|
+
"""Raised when a Dropbox authentication exception occurs."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, status):
|
|
14
|
+
"""Initialize exception."""
|
|
15
|
+
super().__init__(status)
|
|
16
|
+
self.status = status
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True, kw_only=True)
|
|
6
|
+
class UploadSessionStartResponse:
|
|
7
|
+
"""Response from upload session start."""
|
|
8
|
+
|
|
9
|
+
session_id: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, kw_only=True)
|
|
13
|
+
class PropertyField:
|
|
14
|
+
"""A field in a property template."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
type: Literal["string", "number", "boolean", "date", "list", "multi_select"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, kw_only=True)
|
|
22
|
+
class PropertyTemplate:
|
|
23
|
+
"""A property template for files and folders."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
fields: list[PropertyField]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, kw_only=True)
|
|
31
|
+
class PropertyTemplateResponse:
|
|
32
|
+
"""Response from creating a property template."""
|
|
33
|
+
|
|
34
|
+
template_id: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, kw_only=True)
|
|
38
|
+
class PropertyTemplateGetResponse:
|
|
39
|
+
"""Response from getting a property template."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
description: str
|
|
43
|
+
fields: list[PropertyField]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, kw_only=True)
|
|
47
|
+
class PropertyFieldValue:
|
|
48
|
+
"""A field value in a property group."""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
value: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, kw_only=True)
|
|
55
|
+
class PropertyGroup:
|
|
56
|
+
"""A property group containing fields."""
|
|
57
|
+
|
|
58
|
+
fields: list[PropertyFieldValue]
|
|
59
|
+
template_id: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, kw_only=True)
|
|
63
|
+
class FileOrFolderInfo:
|
|
64
|
+
"""File information."""
|
|
65
|
+
|
|
66
|
+
is_folder: bool
|
|
67
|
+
name: str
|
|
68
|
+
size: int | None
|
|
69
|
+
property_groups: list[PropertyGroup] | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, kw_only=True)
|
|
73
|
+
class AccountInfo:
|
|
74
|
+
"""Info about an account."""
|
|
75
|
+
|
|
76
|
+
account_id: str
|
|
77
|
+
email: str
|
|
78
|
+
display_name: str
|
|
File without changes
|