xlwings-utils 25.0.8__py3-none-any.whl → 25.1.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.
Potentially problematic release.
This version of xlwings-utils might be problematic. Click here for more details.
- xlwings_utils/xlwings_utils.py +109 -81
- xlwings_utils/xlwings_utils_old.py +1064 -0
- {xlwings_utils-25.0.8.dist-info → xlwings_utils-25.1.0.dist-info}/METADATA +6 -5
- xlwings_utils-25.1.0.dist-info/RECORD +7 -0
- xlwings_utils-25.0.8.dist-info/RECORD +0 -6
- {xlwings_utils-25.0.8.dist-info → xlwings_utils-25.1.0.dist-info}/WHEEL +0 -0
- {xlwings_utils-25.0.8.dist-info → xlwings_utils-25.1.0.dist-info}/top_level.txt +0 -0
xlwings_utils/xlwings_utils.py
CHANGED
|
@@ -5,53 +5,32 @@
|
|
|
5
5
|
# /_/\_\|_| \_/\_/ |_||_| |_| \__, ||___/ _____ \__,_| \__||_||_||___/
|
|
6
6
|
# |___/ |_____|
|
|
7
7
|
|
|
8
|
-
__version__ = "25.0
|
|
8
|
+
__version__ = "25.1.0"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
import dropbox
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
import os
|
|
14
13
|
import sys
|
|
15
14
|
import math
|
|
16
15
|
import base64
|
|
16
|
+
import requests
|
|
17
|
+
import json
|
|
17
18
|
|
|
18
|
-
dbx = None
|
|
19
19
|
Pythonista = sys.platform == "ios"
|
|
20
|
+
|
|
20
21
|
try:
|
|
21
22
|
import xlwings
|
|
22
23
|
|
|
23
24
|
xlwings = True
|
|
25
|
+
import pyodide_http
|
|
24
26
|
|
|
25
27
|
except ImportError:
|
|
26
28
|
xlwings = False
|
|
27
29
|
|
|
28
30
|
missing = object()
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"""
|
|
33
|
-
tries to update environment variables from the file environ.toml at top level.
|
|
34
|
-
should only be used under Pythonista
|
|
35
|
-
"""
|
|
36
|
-
try:
|
|
37
|
-
import tomlib
|
|
38
|
-
except ModuleNotFoundError:
|
|
39
|
-
import tomli as tomlib
|
|
40
|
-
from pathlib import Path
|
|
41
|
-
import os
|
|
42
|
-
|
|
43
|
-
environ_file = Path("~/Documents").expanduser() / "environ.toml"
|
|
44
|
-
with open(environ_file, "rb") as f:
|
|
45
|
-
d0 = tomlib.load(f)
|
|
46
|
-
d1 = {}
|
|
47
|
-
for k0, v0 in d0.items():
|
|
48
|
-
if isinstance(v0, dict):
|
|
49
|
-
for k1, v1 in v0.items():
|
|
50
|
-
d1[f"{k0}.{k1}".upper()] = v1
|
|
51
|
-
else:
|
|
52
|
-
d1[k0.upper()] = v0
|
|
53
|
-
|
|
54
|
-
os.environ.update(d1)
|
|
32
|
+
_token = None
|
|
33
|
+
missing = object()
|
|
55
34
|
|
|
56
35
|
|
|
57
36
|
def dropbox_init(refresh_token=missing, app_key=missing, app_secret=missing, **kwargs):
|
|
@@ -85,10 +64,10 @@ def dropbox_init(refresh_token=missing, app_key=missing, app_secret=missing, **k
|
|
|
85
64
|
-------
|
|
86
65
|
dropbox object
|
|
87
66
|
"""
|
|
88
|
-
global dbx
|
|
89
67
|
|
|
90
|
-
|
|
91
|
-
|
|
68
|
+
global _token
|
|
69
|
+
if xlwings:
|
|
70
|
+
pyodide_http.patch_all() # to enable chunked mode
|
|
92
71
|
|
|
93
72
|
if refresh_token is missing:
|
|
94
73
|
if "DROPBOX.REFRESH_TOKEN" in os.environ:
|
|
@@ -106,18 +85,22 @@ def dropbox_init(refresh_token=missing, app_key=missing, app_secret=missing, **k
|
|
|
106
85
|
else:
|
|
107
86
|
raise ValueError("no DROPBOX.APP_SECRET found in environment.")
|
|
108
87
|
|
|
109
|
-
|
|
88
|
+
resp = requests.post(
|
|
89
|
+
"https://api.dropbox.com/oauth2/token",
|
|
90
|
+
data={"grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": app_key, "client_secret": app_secret},
|
|
91
|
+
timeout=30,
|
|
92
|
+
)
|
|
110
93
|
try:
|
|
111
|
-
|
|
112
|
-
except
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
except requests.exceptions.HTTPError:
|
|
113
96
|
raise ValueError("invalid dropbox credentials")
|
|
114
|
-
|
|
97
|
+
_token = resp.json()["access_token"]
|
|
115
98
|
|
|
116
99
|
|
|
117
|
-
def
|
|
118
|
-
global
|
|
119
|
-
if
|
|
120
|
-
|
|
100
|
+
def _login_dropbox():
|
|
101
|
+
global _token
|
|
102
|
+
if _token is None:
|
|
103
|
+
dropbox_init() # use environment
|
|
121
104
|
|
|
122
105
|
|
|
123
106
|
def list_dropbox(path="", recursive=False, show_files=True, show_folders=False):
|
|
@@ -151,28 +134,31 @@ def list_dropbox(path="", recursive=False, show_files=True, show_folders=False):
|
|
|
151
134
|
If REFRESH_TOKEN, APP_KEY and APP_SECRET environment variables are specified,
|
|
152
135
|
it is not necessary to call dropbox_init() prior to any dropbox function.
|
|
153
136
|
"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
out.append(entry.path_display)
|
|
169
|
-
if show_folders and isinstance(entry, dropbox.files.FolderMetadata):
|
|
170
|
-
out.append(entry.path_display + "/")
|
|
137
|
+
_login_dropbox()
|
|
138
|
+
|
|
139
|
+
API_RPC = "https://api.dropboxapi.com/2"
|
|
140
|
+
headers = {"Authorization": f"Bearer {_token}", "Content-Type": "application/json"}
|
|
141
|
+
payload = {"path": path, "recursive": recursive, "include_deleted": False}
|
|
142
|
+
r = requests.post("https://api.dropboxapi.com/2/files/list_folder", headers=headers, json=payload, timeout=30)
|
|
143
|
+
r.raise_for_status()
|
|
144
|
+
data = r.json()
|
|
145
|
+
entries = data["entries"]
|
|
146
|
+
while data.get("has_more"):
|
|
147
|
+
r = requests.post(f"{API_RPC}/files/list_folder/continue", headers=headers, json={"cursor": data["cursor"]}, timeout=30)
|
|
148
|
+
r.raise_for_status()
|
|
149
|
+
data = r.json()
|
|
150
|
+
entries.extend(data["entries"])
|
|
171
151
|
|
|
172
|
-
|
|
152
|
+
result = []
|
|
153
|
+
for entry in entries:
|
|
154
|
+
if show_files and entry[".tag"] == "file":
|
|
155
|
+
result.append(entry["path_display"])
|
|
156
|
+
if show_folders and entry[".tag"] == "folder":
|
|
157
|
+
result.append(entry["path_display"] + "/")
|
|
158
|
+
return result
|
|
173
159
|
|
|
174
160
|
|
|
175
|
-
def read_dropbox(dropbox_path
|
|
161
|
+
def read_dropbox(dropbox_path):
|
|
176
162
|
"""
|
|
177
163
|
read_dropbox
|
|
178
164
|
|
|
@@ -183,9 +169,6 @@ def read_dropbox(dropbox_path, max_retries=100):
|
|
|
183
169
|
dropbox_path : str or Pathlib.Path
|
|
184
170
|
path to read from
|
|
185
171
|
|
|
186
|
-
max_retries : int
|
|
187
|
-
number of retries (default: 100)
|
|
188
|
-
|
|
189
172
|
Returns
|
|
190
173
|
-------
|
|
191
174
|
contents of the dropbox file : bytes
|
|
@@ -194,18 +177,20 @@ def read_dropbox(dropbox_path, max_retries=100):
|
|
|
194
177
|
----
|
|
195
178
|
If REFRESH_TOKEN, APP_KEY and APP_SECRET environment variables are specified,
|
|
196
179
|
it is not necessary to call dropbox_init() prior to any dropbox function.
|
|
197
|
-
|
|
198
|
-
As reading from dropbox is very unreliable under pyodide, reading will have to be retried (by default maximum 100 times).
|
|
199
|
-
The number of retries can be found with read_dropbox.retries.
|
|
200
180
|
"""
|
|
201
181
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
182
|
+
_login_dropbox()
|
|
183
|
+
headers = {"Authorization": f"Bearer {_token}", "Dropbox-API-Arg": json.dumps({"path": dropbox_path})}
|
|
184
|
+
with requests.post("https://content.dropboxapi.com/2/files/download", headers=headers, stream=True, timeout=60) as r:
|
|
185
|
+
try:
|
|
186
|
+
r.raise_for_status()
|
|
187
|
+
except requests.exceptions.HTTPError as e:
|
|
188
|
+
raise FileNotFoundError(f"file {dropbox_path} not found. Original message is {e}") from None
|
|
189
|
+
chunks = []
|
|
190
|
+
for chunk in r.iter_content(chunk_size=1024):
|
|
191
|
+
if chunk:
|
|
192
|
+
chunks.append(chunk)
|
|
193
|
+
return b"".join(chunks)
|
|
209
194
|
|
|
210
195
|
|
|
211
196
|
def write_dropbox(dropbox_path, contents):
|
|
@@ -227,8 +212,36 @@ def write_dropbox(dropbox_path, contents):
|
|
|
227
212
|
If REFRESH_TOKEN, APP_KEY and APP_SECRET environment variables are specified,
|
|
228
213
|
it is not necessary to call dropbox_init() prior to any dropbox function.
|
|
229
214
|
"""
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
_login_dropbox()
|
|
216
|
+
headers = {
|
|
217
|
+
"Authorization": f"Bearer {_token}",
|
|
218
|
+
"Dropbox-API-Arg": json.dumps(
|
|
219
|
+
{
|
|
220
|
+
"path": dropbox_path, # Where it will be saved in Dropbox
|
|
221
|
+
"mode": "overwrite", # "add" or "overwrite"
|
|
222
|
+
"autorename": False,
|
|
223
|
+
"mute": False,
|
|
224
|
+
}
|
|
225
|
+
),
|
|
226
|
+
"Content-Type": "application/octet-stream",
|
|
227
|
+
}
|
|
228
|
+
response = requests.post("https://content.dropboxapi.com/2/files/upload", headers=headers, data=contents)
|
|
229
|
+
return response
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def delete_from_dropbox(dropbox_path):
|
|
233
|
+
_login_dropbox()
|
|
234
|
+
|
|
235
|
+
headers = {"Authorization": f"Bearer {_token}", "Content-Type": "application/json"}
|
|
236
|
+
|
|
237
|
+
data = {
|
|
238
|
+
"path": dropbox_path # Path in Dropbox, starting with /
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
response = requests.post("https://api.dropboxapi.com/2/files/delete_v2", headers=headers, data=json.dumps(data))
|
|
242
|
+
if response.status_code == 200:
|
|
243
|
+
return
|
|
244
|
+
raise FileNotFoundError(f"dropbox file {dropbox_path} not found")
|
|
232
245
|
|
|
233
246
|
|
|
234
247
|
def list_local(path, recursive=False, show_files=True, show_folders=False):
|
|
@@ -307,6 +320,8 @@ class block:
|
|
|
307
320
|
self.dict = {}
|
|
308
321
|
self.number_of_rows = number_of_rows
|
|
309
322
|
self.number_of_columns = number_of_columns
|
|
323
|
+
self._highest_used_row_number = None
|
|
324
|
+
self._highest_used_column_number = None
|
|
310
325
|
|
|
311
326
|
def __eq__(self, other):
|
|
312
327
|
if isinstance(other, block):
|
|
@@ -489,8 +504,14 @@ class block:
|
|
|
489
504
|
if value is None:
|
|
490
505
|
if (row, column) in self.dict:
|
|
491
506
|
del self.dict[row, column]
|
|
507
|
+
self._highest_used_row_number = None # invalidate cached value
|
|
508
|
+
self._highest_used_column_number = None # invalidate cached value
|
|
492
509
|
else:
|
|
493
510
|
self.dict[row, column] = value
|
|
511
|
+
if self._highest_used_row_number:
|
|
512
|
+
self._highest_used_row_number = max(self._highest_used_row_number, row)
|
|
513
|
+
if self._highest_used_column_number:
|
|
514
|
+
self._highest_used_column_number = max(self._highest_used_column_number, column)
|
|
494
515
|
|
|
495
516
|
def __getitem__(self, row_column):
|
|
496
517
|
row, column = row_column
|
|
@@ -517,6 +538,7 @@ class block:
|
|
|
517
538
|
def number_of_rows(self, value):
|
|
518
539
|
if value < 1:
|
|
519
540
|
raise ValueError(f"number_of_rows should be >=1, not {value}")
|
|
541
|
+
self._highest_used_row_number = None
|
|
520
542
|
self._number_of_rows = value
|
|
521
543
|
for row, column in list(self.dict):
|
|
522
544
|
if row > self._number_of_rows:
|
|
@@ -530,6 +552,7 @@ class block:
|
|
|
530
552
|
def number_of_columns(self, value):
|
|
531
553
|
if value < 1:
|
|
532
554
|
raise ValueError(f"number_of_columns should be >=1, not {value}")
|
|
555
|
+
self._highest_used_column_number = None
|
|
533
556
|
self._number_of_columns = value
|
|
534
557
|
for row, column in list(self.dict):
|
|
535
558
|
if column > self._number_of_columns:
|
|
@@ -537,17 +560,22 @@ class block:
|
|
|
537
560
|
|
|
538
561
|
@property
|
|
539
562
|
def highest_used_row_number(self):
|
|
540
|
-
if self.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
563
|
+
if not self._highest_used_row_number:
|
|
564
|
+
if self.dict:
|
|
565
|
+
self._highest_used_row_number = max(row for (row, column) in self.dict)
|
|
566
|
+
else:
|
|
567
|
+
self._highest_used_row_number = 1
|
|
568
|
+
return self._highest_used_row_number
|
|
544
569
|
|
|
545
570
|
@property
|
|
546
571
|
def highest_used_column_number(self):
|
|
547
|
-
if self.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
572
|
+
if not self._highest_used_column_number:
|
|
573
|
+
if self.dict:
|
|
574
|
+
self._highest_used_column_number = max(column for (row, column) in self.dict)
|
|
575
|
+
else:
|
|
576
|
+
self._highest_used_column_number = 1
|
|
577
|
+
|
|
578
|
+
return self._highest_used_column_number
|
|
551
579
|
|
|
552
580
|
def __repr__(self):
|
|
553
581
|
return f"block({self.value})"
|