xlwings-utils 25.2.1__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.
@@ -0,0 +1,5 @@
1
+ from .xlwings_utils import *
2
+ from .xlwings_utils import __version__
3
+ from .dropbox import *
4
+ from .nextcloud import *
5
+ from .local import *
@@ -0,0 +1,258 @@
1
+ import os
2
+ import requests
3
+ import json
4
+
5
+ _token = None
6
+ missing = object()
7
+
8
+
9
+ def normalize_path(path):
10
+ path = str(path).strip()
11
+ if path == "/":
12
+ path = ""
13
+ if path and not path.startswith("/"):
14
+ path = "/" + path
15
+ return path
16
+
17
+
18
+ def init(refresh_token=missing, app_key=missing, app_secret=missing, **kwargs):
19
+ """
20
+ This function may to be called prior to using any dropbox function
21
+ to specify the request token, app key and app secret.
22
+ If these are specified as DROPBOX.REFRESH_TOKEN, DROPBOX.APP_KEY and DROPBOX.APP_SECRET
23
+ environment variables, it is not necessary to call dropbox_init().
24
+
25
+ Parameters
26
+ ----------
27
+ refresh_token : str
28
+ oauth2 refreshntoken
29
+
30
+ if omitted: use the environment variable DROPBOX.REFRESH_TOKEN
31
+
32
+ app_key : str
33
+ app key
34
+
35
+ if omitted: use the environment variable DROPBOX.APP_KEY
36
+
37
+
38
+ app_secret : str
39
+ app secret
40
+
41
+ if omitted: use the environment variable DROPBOX.APP_SECRET
42
+
43
+ Returns
44
+ -------
45
+ dropbox object
46
+ """
47
+
48
+ global _token
49
+ try:
50
+ import pyodide_http
51
+
52
+ pyodide_http.patch_all() # required to reliably use requests on pyodide platforms
53
+
54
+ except ImportError:
55
+ ...
56
+
57
+ if refresh_token is missing:
58
+ if "DROPBOX.REFRESH_TOKEN" in os.environ:
59
+ refresh_token = os.environ["DROPBOX.REFRESH_TOKEN"]
60
+ else:
61
+ raise ValueError("no DROPBOX.REFRESH_TOKEN found in environment.")
62
+ if app_key is missing:
63
+ if "DROPBOX.APP_KEY" in os.environ:
64
+ app_key = os.environ["DROPBOX.APP_KEY"]
65
+ else:
66
+ raise ValueError("no DROPBOX.APP_KEY found in environment.")
67
+ if app_secret is missing:
68
+ if "DROPBOX.APP_SECRET" in os.environ:
69
+ app_secret = os.environ["DROPBOX.APP_SECRET"]
70
+ else:
71
+ raise ValueError("no DROPBOX.APP_SECRET found in environment.")
72
+
73
+ response = requests.post(
74
+ "https://api.dropbox.com/oauth2/token",
75
+ data={"grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": app_key, "client_secret": app_secret},
76
+ timeout=30,
77
+ )
78
+ try:
79
+ response.raise_for_status()
80
+ except requests.exceptions.HTTPError:
81
+ raise ValueError("invalid dropbox credentials")
82
+ _token = response.json()["access_token"]
83
+
84
+
85
+ def _login():
86
+ if _token is None:
87
+ init() # use environment
88
+
89
+
90
+ def dir(path="", recursive=False, show_files=True, show_folders=False):
91
+ """
92
+ returns all dropbox files/folders in path
93
+
94
+ Parameters
95
+ ----------
96
+ path : str or Pathlib.Path
97
+ path from which to list all files (default: '')
98
+
99
+ recursive : bool
100
+ if True, recursively list files and folders. if False (default) no recursion
101
+
102
+ show_files : bool
103
+ if True (default), show file entries
104
+ if False, do not show file entries
105
+
106
+ show_folders : bool
107
+ if True, show folder entries
108
+ if False (default), do not show folder entries
109
+
110
+ Returns
111
+ -------
112
+ files : list
113
+
114
+ Note
115
+ ----
116
+ If DROPBOX.REFRESH_TOKEN, DROPBOX.APP_KEY and DROPBOX.APP_SECRET environment variables are specified,
117
+ it is not necessary to call dropbox_init() prior to any dropbox function.
118
+ """
119
+ _login()
120
+
121
+ path = normalize_path(path)
122
+
123
+ API_RPC = "https://api.dropboxapi.com/2"
124
+ headers = {"Authorization": f"Bearer {_token}", "Content-Type": "application/json"}
125
+ payload = {"path": path, "recursive": recursive, "include_deleted": False}
126
+ response = requests.post("https://api.dropboxapi.com/2/files/list_folder", headers=headers, json=payload, timeout=30)
127
+ try:
128
+ response.raise_for_status()
129
+ except requests.exceptions.HTTPError as e:
130
+ raise OSError(f"error listing dropbox. Original message is {e}") from None
131
+ data = response.json()
132
+ entries = data["entries"]
133
+ while data.get("has_more"):
134
+ response = requests.post(f"{API_RPC}/files/list_folder/continue", headers=headers, json={"cursor": data["cursor"]}, timeout=30)
135
+ try:
136
+ response.raise_for_status()
137
+ except requests.exceptions.HTTPError as e:
138
+ raise OSError(f"error listing dropbox. Original message is {e}") from None
139
+ data = response.json()
140
+ entries.extend(data["entries"])
141
+
142
+ result = []
143
+ for entry in entries:
144
+ if show_files and entry[".tag"] == "file":
145
+ result.append(entry["path_display"])
146
+ if show_folders and entry[".tag"] == "folder":
147
+ result.append(entry["path_display"] + "/")
148
+ return result
149
+
150
+
151
+ def read(path):
152
+ """
153
+ read file from dropbox
154
+
155
+ Parameters
156
+ ----------
157
+ path : str or Pathlib.Path
158
+ path to read from
159
+
160
+ Returns
161
+ -------
162
+ contents of the dropbox file : bytes
163
+
164
+ Note
165
+ ----
166
+ If the file could not be read, an OSError will be raised.
167
+
168
+ Note
169
+ ----
170
+ If DROPBOX.REFRESH_TOKEN, DROPBOX.APP_KEY and DROPBOX.APP_SECRET environment variables are specified,
171
+ it is not necessary to call dropbox_init() prior to any dropbox function.
172
+ """
173
+
174
+ _login()
175
+
176
+ path = normalize_path(path)
177
+
178
+ headers = {"Authorization": f"Bearer {_token}", "Dropbox-API-Arg": json.dumps({"path": path})}
179
+ with requests.post("https://content.dropboxapi.com/2/files/download", headers=headers, stream=True, timeout=60) as response:
180
+ try:
181
+ response.raise_for_status()
182
+ except requests.exceptions.HTTPError as e:
183
+ raise OSError(f"file {str(path)} not found. Original message is {e}") from None
184
+ chunks = []
185
+ for chunk in response.iter_content(chunk_size=1024):
186
+ if chunk:
187
+ chunks.append(chunk)
188
+ return b"".join(chunks)
189
+
190
+
191
+ def write(path, contents):
192
+ """
193
+ write to file on dropbox
194
+
195
+ Parameters
196
+ ----------
197
+ path : str or Pathlib.Path
198
+ path to write to
199
+
200
+ contents : bytes
201
+ contents to be written
202
+
203
+ Note
204
+ ----
205
+ If the file could not be written, an OSError will be raised.
206
+
207
+ Note
208
+ ----
209
+ If DROPBOX.REFRESH_TOKEN, DROPBOX.APP_KEY and DROPBOX.APP_SECRET environment variables are specified,
210
+ it is not necessary to call dropbox_init() prior to any dropbox function.
211
+ """
212
+ _login()
213
+ path = normalize_path(path)
214
+
215
+ headers = {
216
+ "Authorization": f"Bearer {_token}",
217
+ "Dropbox-API-Arg": json.dumps(
218
+ {"path": str(path), "mode": "overwrite", "autorename": False, "mute": False} # Where it will be saved in Dropbox # "add" or "overwrite"
219
+ ),
220
+ "Content-Type": "application/octet-stream",
221
+ }
222
+ response = requests.post("https://content.dropboxapi.com/2/files/upload", headers=headers, data=contents)
223
+ try:
224
+ response.raise_for_status()
225
+ except requests.exceptions.HTTPError as e:
226
+ raise OSError(f"file {str(path)} could not be written. Original message is {e}") from None
227
+
228
+
229
+ def delete(path):
230
+ """
231
+ delete file dropbox
232
+
233
+ Parameters
234
+ ----------
235
+ path : str or Pathlib.Path
236
+ path to delete
237
+
238
+ Note
239
+ ----
240
+ If the file could not be deleted, an OSError will be raised.
241
+
242
+ Note
243
+ ----
244
+ If DROPBOX.REFRESH_TOKEN, DROPBOX.APP_KEY and DROPBOX.APP_SECRET environment variables are specified,
245
+ it is not necessary to call dropbox_init() prior to any dropbox function.
246
+ """
247
+ _login()
248
+ path = normalize_path(path)
249
+
250
+ headers = {"Authorization": f"Bearer {_token}", "Content-Type": "application/json"}
251
+
252
+ data = {"path": str(path)} # Path in Dropbox, starting with /
253
+
254
+ response = requests.post("https://api.dropboxapi.com/2/files/delete_v2", headers=headers, data=json.dumps(data))
255
+ try:
256
+ response.raise_for_status()
257
+ except requests.exceptions.HTTPError as e:
258
+ raise OSError(f"file {str(path)} could not be deleted. Original message is {e}") from None
xlwings_utils/local.py ADDED
@@ -0,0 +1,53 @@
1
+ from pathlib import Path
2
+
3
+ def dir(path, recursive=False, show_files=True, show_folders=False):
4
+ """
5
+ returns all local files/folders at given path
6
+
7
+ Parameters
8
+ ----------
9
+ path : str or Pathlib.Path
10
+ path from which to list all files (default: '')
11
+
12
+ recursive : bool
13
+ if True, recursively list files. if False (default) no recursion
14
+
15
+ show_files : bool
16
+ if True (default), show file entries
17
+ if False, do not show file entries
18
+
19
+ show_folders : bool
20
+ if True, show folder entries
21
+ if False (default), do not show folder entries
22
+
23
+ Returns
24
+ -------
25
+ files, relative to path : list
26
+ """
27
+ path = Path(path)
28
+
29
+ result = []
30
+ for entry in path.iterdir():
31
+ if entry.is_file():
32
+ if show_files:
33
+ result.append(str(entry))
34
+ elif entry.is_dir():
35
+ if show_folders:
36
+ result.append(str(entry) + "/")
37
+ if recursive:
38
+ result.extend(list_local(entry, recursive=recursive, show_files=show_files, show_folders=show_folders))
39
+ return result
40
+
41
+
42
+ def write(path, contents):
43
+ path = Path(path)
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ with open(path, "wb") as f:
46
+ f.write(contents)
47
+
48
+
49
+ def read(path):
50
+ path = Path(path)
51
+ with open(path, "rb") as f:
52
+ contents = f.read()
53
+ return contents
@@ -0,0 +1,236 @@
1
+ import requests
2
+ import xml.etree.ElementTree
3
+ import urllib.parse
4
+ import xlwings_utils
5
+ import os
6
+
7
+ try:
8
+ import pyodide_http
9
+ except ImportError:
10
+ ...
11
+
12
+ missing = object()
13
+
14
+ _url = None
15
+
16
+
17
+ def make_base_path(webdav_url: str) -> str:
18
+ """
19
+ Turn 'https://host/remote.php/dav/files/user%40mail.com/'
20
+ into '/remote.php/dav/files/user@mail.com/' (decoded, with trailing slash).
21
+ """
22
+ p = urllib.parse.urlparse(webdav_url)
23
+ path = urllib.parse.unquote(p.path)
24
+ if not path.endswith("/"):
25
+ path += "/"
26
+ return path
27
+
28
+
29
+ def clean_href(href: str, base_path: str) -> str:
30
+ """
31
+ Turn href from PROPFIND into a clean relative path.
32
+ Works whether href is absolute URL or just a server path,
33
+ and whether it's encoded or not.
34
+ """
35
+ # href may be a full URL or just a path
36
+ if "://" in href:
37
+ href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path)
38
+ else:
39
+ href_path = urllib.parse.unquote(href)
40
+
41
+ # Ensure base_path is decoded and slash-normalized
42
+ base_path = urllib.parse.unquote(base_path)
43
+ if not base_path.endswith("/"):
44
+ base_path += "/"
45
+
46
+ if href_path.startswith(base_path):
47
+ rel = href_path[len(base_path) :]
48
+ else:
49
+ # fallback: just strip leading slash to avoid weird output
50
+ rel = href_path.lstrip("/")
51
+
52
+ return rel.lstrip("/")
53
+
54
+
55
+ def init(url=missing, username=missing, password=missing, **kwargs):
56
+ global _auth
57
+ global _url
58
+ try:
59
+ import pyodide_http
60
+
61
+ pyodide_http.patch_all() # required to reliably use requests on pyodide platforms
62
+
63
+ except ImportError:
64
+ ...
65
+
66
+ if url is missing:
67
+ if "NEXTCLOUD.URL" in os.environ:
68
+ url = os.environ["NEXTCLOUD.URL"]
69
+ else:
70
+ raise ValueError("no NEXTCLOUD.URL found in environment.")
71
+ if username is missing:
72
+ if "NEXTCLOUD.USERNAME" in os.environ:
73
+ username = os.environ["NEXTCLOUD.USERNAME"]
74
+ else:
75
+ raise ValueError("no NEXTCLOUD.USERNAMEfound in environment.")
76
+ if password is missing:
77
+ if "NEXTCLOUD.PASSWORD" in os.environ:
78
+ password = os.environ["NEXTCLOUD.PASSWORD"]
79
+ else:
80
+ raise ValueError("no NEXTCLOUD.PASSWORD found in environment.")
81
+ _url = url
82
+ _auth = (username, password)
83
+
84
+
85
+ def _login():
86
+ global _url
87
+ if _url is None:
88
+ init() # use environment
89
+
90
+
91
+ def dir(path="", recursive=False, show_files=True, show_folders=False):
92
+ """
93
+ returns all nextcloud files/folders in path
94
+
95
+ Parameters
96
+ ----------
97
+ path : str or Pathlib.Path
98
+ path from which to list all files (default: '')
99
+
100
+ recursive : bool
101
+ if True, recursively list files and folders. if False (default) no recursion
102
+
103
+ show_files : bool
104
+ if True (default), show file entries
105
+ if False, do not show file entries
106
+
107
+ show_folders : bool
108
+ if True, show folder entries
109
+ if False (default), do not show folder entries
110
+
111
+ Returns
112
+ -------
113
+ files : list
114
+
115
+ Note
116
+ ----
117
+ If NEXTCLOUD.URL, NEXTCLOUD.USERNAME and NEXTCLOUD.PASSWORD environment variables are specified,
118
+ it is not necessary to call nextcloud_init() prior to any nextcloud function.
119
+ """
120
+ _login()
121
+
122
+ headers = {"Depth": "1000" if recursive else "1"} # 1 = directory + its immediate children
123
+
124
+ response = requests.request("PROPFIND", _url + path, auth=_auth, headers=headers)
125
+
126
+ response.raise_for_status()
127
+ root = xml.etree.ElementTree.fromstring(response.text)
128
+ namespaces = {"d": "DAV:"}
129
+
130
+ items = []
131
+
132
+ base_path = make_base_path(_url)
133
+
134
+ for response_el in root.findall("d:response", namespaces):
135
+ href = response_el.find("d:href", namespaces).text
136
+ href = clean_href(href, base_path)
137
+ if not href.startswith("/"):
138
+ href = "/" + href
139
+
140
+ prop = response_el.find("d:propstat/d:prop", namespaces)
141
+ res_type = prop.find("d:resourcetype", namespaces)
142
+ is_dir = res_type.find("d:collection", namespaces) is not None
143
+ if is_dir and show_folders:
144
+ items.append(href)
145
+ if not is_dir and show_files:
146
+ items.append(href)
147
+ return items
148
+
149
+
150
+ def read(path):
151
+ """
152
+ read file from nextcloud
153
+
154
+ Parameters
155
+ ----------
156
+ path : str or Pathlib.Path
157
+ path to read from
158
+
159
+ Returns
160
+ -------
161
+ contents of the nextcloud file : bytes
162
+
163
+ Note
164
+ ----
165
+ If the file could not be read, an OSError will be raised.
166
+
167
+ Note
168
+ ----
169
+ If NEXTCLOUD.URL, NEXTCLOUD.USERNAME and NEXTCLOUD.PASSWORD environment variables are specified,
170
+ it is not necessary to call nextcloud_init() prior to any nextcloud function.
171
+ """
172
+
173
+ _login()
174
+ response = requests.get(_url + path, auth=_auth)
175
+ try:
176
+ response.raise_for_status()
177
+ except requests.exceptions.HTTPError as e:
178
+ raise OSError(f"file {str(path)} not found. Original message is {e}") from None
179
+ file_content = response.content
180
+ return file_content
181
+
182
+
183
+ def write(path, contents):
184
+ """
185
+ write to file on nextcloud
186
+
187
+ Parameters
188
+ ----------
189
+ path : str or Pathlib.Path
190
+ path to write to
191
+
192
+ contents : bytes
193
+ contents to be written
194
+
195
+ Note
196
+ ----
197
+ If the file could not be written, an OSError will be raised.
198
+
199
+ Note
200
+ ----
201
+ If NEXTCLOUD.URL, NEXTCLOUD.USERNAME and NEXTCLOUD.PASSWORD environment variables are specified,
202
+ it is not necessary to call nextcloud_init() prior to any nextcloud function.
203
+ """
204
+ _login()
205
+ response = requests.put(_url + path, auth=_auth, data=contents, timeout=60)
206
+ try:
207
+ response.raise_for_status()
208
+ except requests.exceptions.HTTPError as e:
209
+ raise OSError(f"file {str(path)} could not be written. Original message is {e}") from None
210
+
211
+
212
+ def delete(path):
213
+ """
214
+ delete file nextcloud
215
+
216
+ Parameters
217
+ ----------
218
+ path : str or Pathlib.Path
219
+ path to delete
220
+
221
+ Note
222
+ ----
223
+ If the file could not be deleted, an OSError will be raised.
224
+
225
+ Note
226
+ ----
227
+ If NEXTCLOUD.URL, NEXTCLOUD.USERNAME and NEXTCLOUD.PASSWORD environment variables are specified,
228
+ it is not necessary to call nextcloud_init() prior to any nextcloud function.
229
+ """
230
+ _login()
231
+
232
+ response = requests.delete(_url + path, auth=_auth, timeout=30)
233
+ try:
234
+ response.raise_for_status()
235
+ except requests.exceptions.HTTPError as e:
236
+ raise OSError(f"file {str(path)} could not be deleted. Original message is {e}") from None