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.

@@ -5,53 +5,32 @@
5
5
  # /_/\_\|_| \_/\_/ |_||_| |_| \__, ||___/ _____ \__,_| \__||_||_||___/
6
6
  # |___/ |_____|
7
7
 
8
- __version__ = "25.0.8"
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
- def pythonista_environ():
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
- if Pythonista:
91
- pythonista_environ()
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
- _dbx = dropbox.Dropbox(oauth2_refresh_token=refresh_token, app_key=app_key, app_secret=app_secret, **kwargs)
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
- _dbx.files_list_folder(path="") # just to test proper credentials
112
- except dropbox.exceptions.AuthError:
94
+ resp.raise_for_status()
95
+ except requests.exceptions.HTTPError:
113
96
  raise ValueError("invalid dropbox credentials")
114
- return _dbx
97
+ _token = resp.json()["access_token"]
115
98
 
116
99
 
117
- def _login_dbx():
118
- global dbx
119
- if dbx is None:
120
- dbx = dropbox_init() # use environment
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
- _login_dbx()
155
- out = []
156
- result = dbx.files_list_folder(path, recursive=recursive)
157
-
158
- for entry in result.entries:
159
- if show_files and isinstance(entry, dropbox.files.FileMetadata):
160
- out.append(entry.path_display)
161
- if show_folders and isinstance(entry, dropbox.files.FolderMetadata):
162
- out.append(entry.path_display + "/")
163
-
164
- while result.has_more:
165
- result = dbx.files_list_folder_continue(result.cursor)
166
- for entry in result.entries:
167
- if show_files and isinstance(entry, dropbox.files.FileMetadata):
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
- return out
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, max_retries=100):
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
- _login_dbx()
203
- for read_dropbox.retries in range(max_retries + 1):
204
- metadata, response = dbx.files_download(dropbox_path)
205
- file_content = response.content
206
- if len(file_content) == metadata.size:
207
- return file_content
208
- raise OSError(f"after {max_retries} still no valid response")
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
- _login_dbx()
231
- dbx.files_upload(contents, dropbox_path, mode=dropbox.files.WriteMode.overwrite)
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.dict:
541
- return max(row for (row, column) in self.dict)
542
- else:
543
- return 1
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.dict:
548
- return max(column for (row, column) in self.dict)
549
- else:
550
- return 1
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})"