tdrpa.tdworker 1.2.13.2__py312-none-win_amd64.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.
- tdrpa/_tdxlwings/__init__.py +193 -0
- tdrpa/_tdxlwings/__pycache__/__init__.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/__init__.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_win32patch.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/_xlwindows.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/apps.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/apps.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/base_classes.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/base_classes.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/com_server.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/com_server.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/constants.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/constants.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/expansion.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/expansion.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/main.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/main.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/udfs.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/udfs.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/utils.cpython-311.pyc +0 -0
- tdrpa/_tdxlwings/__pycache__/utils.cpython-38.pyc +0 -0
- tdrpa/_tdxlwings/_win32patch.py +90 -0
- tdrpa/_tdxlwings/_xlmac.py +2240 -0
- tdrpa/_tdxlwings/_xlwindows.py +2518 -0
- tdrpa/_tdxlwings/addin/Dictionary.cls +474 -0
- tdrpa/_tdxlwings/addin/IWebAuthenticator.cls +71 -0
- tdrpa/_tdxlwings/addin/WebClient.cls +772 -0
- tdrpa/_tdxlwings/addin/WebHelpers.bas +3203 -0
- tdrpa/_tdxlwings/addin/WebRequest.cls +875 -0
- tdrpa/_tdxlwings/addin/WebResponse.cls +453 -0
- tdrpa/_tdxlwings/addin/xlwings.xlam +0 -0
- tdrpa/_tdxlwings/apps.py +35 -0
- tdrpa/_tdxlwings/base_classes.py +1092 -0
- tdrpa/_tdxlwings/cli.py +1306 -0
- tdrpa/_tdxlwings/com_server.py +385 -0
- tdrpa/_tdxlwings/constants.py +3080 -0
- tdrpa/_tdxlwings/conversion/__init__.py +103 -0
- tdrpa/_tdxlwings/conversion/framework.py +147 -0
- tdrpa/_tdxlwings/conversion/numpy_conv.py +34 -0
- tdrpa/_tdxlwings/conversion/pandas_conv.py +184 -0
- tdrpa/_tdxlwings/conversion/standard.py +321 -0
- tdrpa/_tdxlwings/expansion.py +83 -0
- tdrpa/_tdxlwings/ext/__init__.py +3 -0
- tdrpa/_tdxlwings/ext/sql.py +73 -0
- tdrpa/_tdxlwings/html/xlwings-alert.html +71 -0
- tdrpa/_tdxlwings/js/xlwings.js +577 -0
- tdrpa/_tdxlwings/js/xlwings.ts +729 -0
- tdrpa/_tdxlwings/mac_dict.py +6399 -0
- tdrpa/_tdxlwings/main.py +5205 -0
- tdrpa/_tdxlwings/mistune/__init__.py +63 -0
- tdrpa/_tdxlwings/mistune/block_parser.py +366 -0
- tdrpa/_tdxlwings/mistune/inline_parser.py +216 -0
- tdrpa/_tdxlwings/mistune/markdown.py +84 -0
- tdrpa/_tdxlwings/mistune/renderers.py +220 -0
- tdrpa/_tdxlwings/mistune/scanner.py +121 -0
- tdrpa/_tdxlwings/mistune/util.py +41 -0
- tdrpa/_tdxlwings/pro/__init__.py +40 -0
- tdrpa/_tdxlwings/pro/_xlcalamine.py +536 -0
- tdrpa/_tdxlwings/pro/_xlofficejs.py +146 -0
- tdrpa/_tdxlwings/pro/_xlremote.py +1293 -0
- tdrpa/_tdxlwings/pro/custom_functions_code.js +150 -0
- tdrpa/_tdxlwings/pro/embedded_code.py +60 -0
- tdrpa/_tdxlwings/pro/udfs_officejs.py +549 -0
- tdrpa/_tdxlwings/pro/utils.py +199 -0
- tdrpa/_tdxlwings/quickstart.xlsm +0 -0
- tdrpa/_tdxlwings/quickstart_addin.xlam +0 -0
- tdrpa/_tdxlwings/quickstart_addin_ribbon.xlam +0 -0
- tdrpa/_tdxlwings/quickstart_fastapi/main.py +47 -0
- tdrpa/_tdxlwings/quickstart_fastapi/requirements.txt +3 -0
- tdrpa/_tdxlwings/quickstart_standalone.xlsm +0 -0
- tdrpa/_tdxlwings/reports.py +12 -0
- tdrpa/_tdxlwings/rest/__init__.py +1 -0
- tdrpa/_tdxlwings/rest/api.py +368 -0
- tdrpa/_tdxlwings/rest/serializers.py +103 -0
- tdrpa/_tdxlwings/server.py +14 -0
- tdrpa/_tdxlwings/udfs.py +775 -0
- tdrpa/_tdxlwings/utils.py +777 -0
- tdrpa/_tdxlwings/xlwings-0.31.6.applescript +30 -0
- tdrpa/_tdxlwings/xlwings.bas +2061 -0
- tdrpa/_tdxlwings/xlwings_custom_addin.bas +2042 -0
- tdrpa/_tdxlwings/xlwingslib.cp38-win_amd64.pyd +0 -0
- tdrpa/tdworker/__init__.pyi +12 -0
- tdrpa/tdworker/_clip.pyi +50 -0
- tdrpa/tdworker/_excel.pyi +743 -0
- tdrpa/tdworker/_file.pyi +77 -0
- tdrpa/tdworker/_img.pyi +226 -0
- tdrpa/tdworker/_network.pyi +94 -0
- tdrpa/tdworker/_os.pyi +47 -0
- tdrpa/tdworker/_sp.pyi +21 -0
- tdrpa/tdworker/_w.pyi +129 -0
- tdrpa/tdworker/_web.pyi +995 -0
- tdrpa/tdworker/_winE.pyi +228 -0
- tdrpa/tdworker/_winK.pyi +74 -0
- tdrpa/tdworker/_winM.pyi +117 -0
- tdrpa/tdworker.cp312-win_amd64.pyd +0 -0
- tdrpa_tdworker-1.2.13.2.dist-info/METADATA +38 -0
- tdrpa_tdworker-1.2.13.2.dist-info/RECORD +101 -0
- tdrpa_tdworker-1.2.13.2.dist-info/WHEEL +5 -0
- tdrpa_tdworker-1.2.13.2.dist-info/top_level.txt +1 -0
tdrpa/_tdxlwings/cli.py
ADDED
@@ -0,0 +1,1306 @@
|
|
1
|
+
import argparse
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
import subprocess
|
6
|
+
import sys
|
7
|
+
import time
|
8
|
+
import uuid
|
9
|
+
from keyword import iskeyword
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
import tdrpa._tdxlwings as xw
|
13
|
+
|
14
|
+
# Directories/paths
|
15
|
+
this_dir = Path(__file__).resolve().parent
|
16
|
+
|
17
|
+
|
18
|
+
def auth_aad(args):
|
19
|
+
_auth_aad(
|
20
|
+
tenant_id=args.tenant_id,
|
21
|
+
client_id=args.client_id,
|
22
|
+
port=args.port,
|
23
|
+
scopes=args.scopes,
|
24
|
+
username=args.username,
|
25
|
+
reset=args.reset,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
def _auth_aad(
|
30
|
+
client_id=None, tenant_id=None, username=None, port=None, scopes=None, reset=False
|
31
|
+
):
|
32
|
+
from tdrpa._tdxlwings.utils import read_user_config
|
33
|
+
|
34
|
+
try:
|
35
|
+
import msal
|
36
|
+
except ImportError:
|
37
|
+
sys.exit("Couldn't find the 'msal' package. Install it via `pip install msal`.")
|
38
|
+
|
39
|
+
cache_dir = Path(xw.USER_CONFIG_FILE).parent
|
40
|
+
cache_file = cache_dir / "aad.json"
|
41
|
+
|
42
|
+
if reset:
|
43
|
+
if cache_file.exists():
|
44
|
+
cache_file.unlink()
|
45
|
+
update_user_config("AZUREAD_ACCESS_TOKEN", None, action="delete")
|
46
|
+
update_user_config("AZUREAD_ACCESS_TOKEN_EXPIRES_ON", None, action="delete")
|
47
|
+
|
48
|
+
user_config = read_user_config()
|
49
|
+
if tenant_id is None:
|
50
|
+
tenant_id = user_config["azuread_tenant_id"]
|
51
|
+
if client_id is None:
|
52
|
+
client_id = user_config["azuread_client_id"]
|
53
|
+
if scopes is None:
|
54
|
+
scopes = user_config["azuread_scopes"]
|
55
|
+
if port is None:
|
56
|
+
port = user_config.get("azuread_port")
|
57
|
+
if username is None:
|
58
|
+
username = user_config.get("azuread_username")
|
59
|
+
# Scopes can only be from one application!
|
60
|
+
if scopes is None:
|
61
|
+
scopes = [""]
|
62
|
+
elif isinstance(scopes, str):
|
63
|
+
scopes = [scope.strip() for scope in scopes.split(",")]
|
64
|
+
else:
|
65
|
+
sys.exit("Please provide scopes as a single string with commas.")
|
66
|
+
|
67
|
+
# https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#authority
|
68
|
+
authority = f"https://login.microsoftonline.com/{tenant_id}"
|
69
|
+
|
70
|
+
# Cache
|
71
|
+
token_cache = msal.SerializableTokenCache()
|
72
|
+
|
73
|
+
cache_file.parent.mkdir(exist_ok=True)
|
74
|
+
if cache_file.exists():
|
75
|
+
token_cache.deserialize(cache_file.read_text())
|
76
|
+
|
77
|
+
app = msal.PublicClientApplication(
|
78
|
+
client_id=client_id,
|
79
|
+
authority=authority,
|
80
|
+
token_cache=token_cache,
|
81
|
+
)
|
82
|
+
|
83
|
+
result = None
|
84
|
+
# Username selects the account if multiple accounts are logged in
|
85
|
+
# This requires scopes to be set though, but scopes=[""] seem to do the trick.
|
86
|
+
accounts = app.get_accounts(username=username if username else None)
|
87
|
+
if accounts:
|
88
|
+
account = accounts[0]
|
89
|
+
result = app.acquire_token_silent(scopes=scopes, account=account)
|
90
|
+
if not result:
|
91
|
+
result = app.acquire_token_interactive(
|
92
|
+
scopes=scopes, timeout=60, port=int(port) if port else None
|
93
|
+
)
|
94
|
+
|
95
|
+
if "access_token" not in result:
|
96
|
+
sys.exit(result.get("error_description"))
|
97
|
+
|
98
|
+
update_user_config(f"AZUREAD_ACCESS_TOKEN_{client_id}", result["access_token"])
|
99
|
+
update_user_config(
|
100
|
+
f"AZUREAD_ACCESS_TOKEN_EXPIRES_ON_{client_id}",
|
101
|
+
int(time.time()) + result["expires_in"],
|
102
|
+
)
|
103
|
+
|
104
|
+
if token_cache.has_state_changed:
|
105
|
+
cache_file.write_text(token_cache.serialize())
|
106
|
+
|
107
|
+
|
108
|
+
def exit_unsupported_platform():
|
109
|
+
if not sys.platform.startswith("win"):
|
110
|
+
sys.exit("Error: This command is currently only supported on Windows.")
|
111
|
+
|
112
|
+
|
113
|
+
def get_addin_dir(global_location=False):
|
114
|
+
# The call to startup_path creates the XLSTART folder if it doesn't exist yet
|
115
|
+
# The global XLSTART folder seems to be always existing
|
116
|
+
if xw.apps:
|
117
|
+
if global_location:
|
118
|
+
addin_dir = Path(xw.apps.active.path) / "XLSTART"
|
119
|
+
addin_dir.mkdir(exist_ok=True)
|
120
|
+
return addin_dir
|
121
|
+
else:
|
122
|
+
return xw.apps.active.startup_path
|
123
|
+
else:
|
124
|
+
with xw.App(visible=False) as app:
|
125
|
+
if global_location:
|
126
|
+
addin_dir = Path(app.path) / "XLSTART"
|
127
|
+
addin_dir.mkdir(exist_ok=True)
|
128
|
+
return addin_dir
|
129
|
+
else:
|
130
|
+
return app.startup_path
|
131
|
+
|
132
|
+
|
133
|
+
def _handle_addin_glob_arg(args):
|
134
|
+
if args.glob:
|
135
|
+
if not sys.platform.startswith("win"):
|
136
|
+
sys.exit("Error: The '--glob' option is only supported on Windows.")
|
137
|
+
return True
|
138
|
+
else:
|
139
|
+
return False
|
140
|
+
|
141
|
+
|
142
|
+
def addin_install(args):
|
143
|
+
global_install = _handle_addin_glob_arg(args)
|
144
|
+
if args.dir:
|
145
|
+
for addin_source_path in Path(args.dir).resolve().glob("[!~$]*.xl*"):
|
146
|
+
_addin_install(str(addin_source_path), global_install)
|
147
|
+
elif args.file:
|
148
|
+
addin_source_path = os.path.abspath(args.file)
|
149
|
+
_addin_install(addin_source_path, global_install)
|
150
|
+
else:
|
151
|
+
addin_source_path = os.path.join(this_dir, "addin", "xlwings.xlam")
|
152
|
+
_addin_install(addin_source_path, global_install)
|
153
|
+
|
154
|
+
|
155
|
+
def _addin_install(addin_source_path, global_location=False):
|
156
|
+
addin_name = os.path.basename(addin_source_path)
|
157
|
+
addin_target_path = os.path.join(get_addin_dir(global_location), addin_name)
|
158
|
+
|
159
|
+
# Close any open add-ins
|
160
|
+
if xw.apps:
|
161
|
+
for app in xw.apps:
|
162
|
+
try:
|
163
|
+
app.books[addin_name].close()
|
164
|
+
except KeyError:
|
165
|
+
pass
|
166
|
+
try:
|
167
|
+
# Install the add-in
|
168
|
+
shutil.copyfile(addin_source_path, addin_target_path)
|
169
|
+
# Load the add-in
|
170
|
+
if xw.apps:
|
171
|
+
for app in xw.apps:
|
172
|
+
if sys.platform.startswith("win"):
|
173
|
+
try:
|
174
|
+
app.books.open(Path(app.startup_path) / addin_name)
|
175
|
+
print("Successfully installed the xlwings add-in!")
|
176
|
+
except: # noqa: E722
|
177
|
+
print(
|
178
|
+
"Successfully installed the xlwings add-in! "
|
179
|
+
"Please restart Excel."
|
180
|
+
)
|
181
|
+
try:
|
182
|
+
app.activate(steal_focus=True)
|
183
|
+
except: # noqa: E722
|
184
|
+
pass
|
185
|
+
else:
|
186
|
+
# macOS asks to explicitly enable macros when opened directly
|
187
|
+
# which isn't a good UX
|
188
|
+
print(
|
189
|
+
"Successfully installed the xlwings add-in! "
|
190
|
+
"Please restart Excel (quit via Cmd-Q, then start Excel again)."
|
191
|
+
)
|
192
|
+
else:
|
193
|
+
print("Successfully installed the xlwings add-in! ")
|
194
|
+
if sys.platform.startswith("darwin"):
|
195
|
+
runpython_install(None)
|
196
|
+
if addin_name == "xlwings.xlam":
|
197
|
+
config_create(None)
|
198
|
+
except IOError as e:
|
199
|
+
if e.args[0] == 13:
|
200
|
+
print(
|
201
|
+
"Error: Failed to install the add-in: If Excel is running, "
|
202
|
+
"quit Excel and try again. If you are using the '--glob' option "
|
203
|
+
"make sure to run this command from an Elevated Command Prompt."
|
204
|
+
)
|
205
|
+
else:
|
206
|
+
print(repr(e))
|
207
|
+
except Exception as e:
|
208
|
+
print(repr(e))
|
209
|
+
|
210
|
+
|
211
|
+
def addin_remove(args):
|
212
|
+
global_install = _handle_addin_glob_arg(args)
|
213
|
+
if args.dir:
|
214
|
+
for addin_source_path in Path(args.dir).resolve().glob("[!~$]*.xl*"):
|
215
|
+
_addin_remove(addin_source_path, global_install)
|
216
|
+
elif args.file:
|
217
|
+
_addin_remove(args.file, global_install)
|
218
|
+
else:
|
219
|
+
_addin_remove("xlwings.xlam", global_install)
|
220
|
+
|
221
|
+
|
222
|
+
def _addin_remove(addin_name, global_install):
|
223
|
+
addin_name = os.path.basename(addin_name)
|
224
|
+
addin_path = os.path.join(get_addin_dir(global_install), addin_name)
|
225
|
+
try:
|
226
|
+
if xw.apps:
|
227
|
+
for app in xw.apps:
|
228
|
+
try:
|
229
|
+
app.books[addin_name].close()
|
230
|
+
except KeyError:
|
231
|
+
pass
|
232
|
+
os.remove(addin_path)
|
233
|
+
print("Successfully removed the add-in!")
|
234
|
+
except (WindowsError, PermissionError) as e:
|
235
|
+
if e.args[0] in (13, 32):
|
236
|
+
print(
|
237
|
+
"Error: Failed to remove the add-in: If Excel is running, "
|
238
|
+
"quit Excel and try again. If you use the '--glob' option, make "
|
239
|
+
"sure to run this command from an Elevated Command Prompt!"
|
240
|
+
"You can also delete it manually from {0}".format(addin_path)
|
241
|
+
)
|
242
|
+
elif e.args[0] == 2:
|
243
|
+
print(
|
244
|
+
"Error: Could not remove the add-in. "
|
245
|
+
"The add-in doesn't seem to be installed."
|
246
|
+
)
|
247
|
+
else:
|
248
|
+
print(repr(e))
|
249
|
+
except Exception as e:
|
250
|
+
print(repr(e))
|
251
|
+
|
252
|
+
|
253
|
+
def addin_status(args):
|
254
|
+
global_install = _handle_addin_glob_arg(args)
|
255
|
+
if args.file:
|
256
|
+
addin_name = os.path.basename(args.file)
|
257
|
+
else:
|
258
|
+
addin_name = "xlwings.xlam"
|
259
|
+
addin_path = os.path.join(get_addin_dir(global_install), addin_name)
|
260
|
+
if os.path.isfile(addin_path):
|
261
|
+
print("The add-in is installed at {}".format(addin_path))
|
262
|
+
print('Use "xlwings addin remove" to uninstall it.')
|
263
|
+
else:
|
264
|
+
print("The add-in is not installed.")
|
265
|
+
print('"xlwings addin install" will install it at: {}'.format(addin_path))
|
266
|
+
|
267
|
+
|
268
|
+
def quickstart(args):
|
269
|
+
project_name = args.project_name
|
270
|
+
if not project_name.isidentifier() or iskeyword(project_name):
|
271
|
+
sys.exit(
|
272
|
+
"Error: You must choose a project name that works as Python module, "
|
273
|
+
"i.e., it must only use letters, underscores and numbers and must not "
|
274
|
+
"start with a number. Note that you *can* rename your Excel file "
|
275
|
+
"manually after running this command, if you also adjust your RunPython "
|
276
|
+
"VBA function accordingly."
|
277
|
+
)
|
278
|
+
cwd = os.getcwd()
|
279
|
+
|
280
|
+
# Project dir
|
281
|
+
project_path = os.path.join(cwd, project_name)
|
282
|
+
if args.fastapi:
|
283
|
+
# Raises an error on its own if the dir already exists
|
284
|
+
shutil.copytree(
|
285
|
+
Path(this_dir) / "quickstart_fastapi",
|
286
|
+
Path(cwd) / project_name,
|
287
|
+
ignore=shutil.ignore_patterns("__pycache__"),
|
288
|
+
)
|
289
|
+
else:
|
290
|
+
if not os.path.exists(project_path):
|
291
|
+
os.makedirs(project_path)
|
292
|
+
else:
|
293
|
+
sys.exit("Error: Directory already exists.")
|
294
|
+
|
295
|
+
# Python file
|
296
|
+
if not args.fastapi:
|
297
|
+
with open(os.path.join(project_path, project_name + ".py"), "w") as f:
|
298
|
+
f.write("import tdrpa._tdxlwings as xw\n\n\n")
|
299
|
+
f.write("def main():\n")
|
300
|
+
f.write(" wb = xw.Book.caller()\n")
|
301
|
+
f.write(" sheet = wb.sheets[0]\n")
|
302
|
+
f.write(' if sheet["A1"].value == "Hello xlwings!":\n')
|
303
|
+
f.write(' sheet["A1"].value = "Bye xlwings!"\n')
|
304
|
+
f.write(" else:\n")
|
305
|
+
f.write(' sheet["A1"].value = "Hello xlwings!"\n\n\n')
|
306
|
+
if sys.platform.startswith("win"):
|
307
|
+
f.write("@xw.func\n")
|
308
|
+
f.write("def hello(name):\n")
|
309
|
+
f.write(' return f"Hello {name}!"\n\n\n')
|
310
|
+
f.write('if __name__ == "__main__":\n')
|
311
|
+
f.write(' xw.Book("{0}.xlsm").set_mock_caller()\n'.format(project_name))
|
312
|
+
f.write(" main()\n")
|
313
|
+
|
314
|
+
# Excel file
|
315
|
+
if args.standalone:
|
316
|
+
source_file = os.path.join(this_dir, "quickstart_standalone.xlsm")
|
317
|
+
elif args.addin and args.ribbon:
|
318
|
+
source_file = os.path.join(this_dir, "quickstart_addin_ribbon.xlam")
|
319
|
+
elif args.addin:
|
320
|
+
source_file = os.path.join(this_dir, "quickstart_addin.xlam")
|
321
|
+
else:
|
322
|
+
source_file = os.path.join(this_dir, "quickstart.xlsm")
|
323
|
+
|
324
|
+
target_file = os.path.join(
|
325
|
+
project_path, project_name + os.path.splitext(source_file)[1]
|
326
|
+
)
|
327
|
+
shutil.copyfile(
|
328
|
+
source_file,
|
329
|
+
target_file,
|
330
|
+
)
|
331
|
+
|
332
|
+
if args.standalone and args.fastapi:
|
333
|
+
book = xw.Book(target_file)
|
334
|
+
import_remote_modules(book)
|
335
|
+
book.save()
|
336
|
+
|
337
|
+
|
338
|
+
def runpython_install(args):
|
339
|
+
destination_dir = (
|
340
|
+
os.path.expanduser("~") + "/Library/Application Scripts/com.microsoft.Excel"
|
341
|
+
)
|
342
|
+
if not os.path.exists(destination_dir):
|
343
|
+
os.makedirs(destination_dir)
|
344
|
+
shutil.copy(
|
345
|
+
os.path.join(this_dir, f"xlwings-{xw.__version__}.applescript"), destination_dir
|
346
|
+
)
|
347
|
+
if args:
|
348
|
+
# Don't print when called as part of "xlwings addin install"
|
349
|
+
print("Successfully enabled RunPython!")
|
350
|
+
|
351
|
+
|
352
|
+
def restapi_run(args):
|
353
|
+
import subprocess
|
354
|
+
|
355
|
+
try:
|
356
|
+
import flask # noqa: F401
|
357
|
+
except ImportError:
|
358
|
+
sys.exit("To use the xlwings REST API server, you need Flask>=1.0.0 installed.")
|
359
|
+
host = args.host
|
360
|
+
port = args.port
|
361
|
+
|
362
|
+
os.environ["FLASK_APP"] = "xlwings.rest.api"
|
363
|
+
subprocess.check_call(["flask", "run", "--host", host, "--port", port])
|
364
|
+
|
365
|
+
|
366
|
+
def license_update(args):
|
367
|
+
"""license handler for xlwings PRO"""
|
368
|
+
key = args.key
|
369
|
+
if not key:
|
370
|
+
sys.exit(
|
371
|
+
"Please provide a license key via the -k/--key option. "
|
372
|
+
"For example: xlwings license update -k MY_KEY"
|
373
|
+
)
|
374
|
+
update_user_config("LICENSE_KEY", key)
|
375
|
+
print("Successfully updated license key.")
|
376
|
+
|
377
|
+
|
378
|
+
def update_user_config(key, value=None, action="add"):
|
379
|
+
# action: 'add' or 'remove'
|
380
|
+
new_config = []
|
381
|
+
if os.path.exists(xw.USER_CONFIG_FILE):
|
382
|
+
with open(xw.USER_CONFIG_FILE, "r") as f:
|
383
|
+
config = f.readlines()
|
384
|
+
for line in config:
|
385
|
+
# Remove existing key and empty lines
|
386
|
+
if line.split(",")[0] == f'"{key}"' or line in ("\r\n", "\n"):
|
387
|
+
pass
|
388
|
+
else:
|
389
|
+
new_config.append(line)
|
390
|
+
if action == "add":
|
391
|
+
new_config.append(f'"{key}","{value}"\n')
|
392
|
+
else:
|
393
|
+
if action == "add":
|
394
|
+
new_config = [f'"{key}","{value}"\n']
|
395
|
+
else:
|
396
|
+
return
|
397
|
+
if not os.path.exists(os.path.dirname(xw.USER_CONFIG_FILE)):
|
398
|
+
os.makedirs(os.path.dirname(xw.USER_CONFIG_FILE))
|
399
|
+
with open(xw.USER_CONFIG_FILE, "w") as f:
|
400
|
+
f.writelines(new_config)
|
401
|
+
|
402
|
+
|
403
|
+
def license_deploy(args):
|
404
|
+
from .pro import LicenseHandler
|
405
|
+
|
406
|
+
print(LicenseHandler.create_deploy_key())
|
407
|
+
|
408
|
+
|
409
|
+
def get_conda_settings():
|
410
|
+
conda_env = os.getenv("CONDA_DEFAULT_ENV")
|
411
|
+
conda_exe = os.getenv("CONDA_EXE")
|
412
|
+
|
413
|
+
if conda_env and conda_exe:
|
414
|
+
# xlwings currently expects the path
|
415
|
+
# without the trailing /bin/conda or \Scripts\conda.exe
|
416
|
+
conda_path = os.path.sep.join(conda_exe.split(os.path.sep)[:-2])
|
417
|
+
return conda_path, conda_env
|
418
|
+
else:
|
419
|
+
return None, None
|
420
|
+
|
421
|
+
|
422
|
+
def config_create(args):
|
423
|
+
if args is None:
|
424
|
+
force = False
|
425
|
+
else:
|
426
|
+
force = args.force
|
427
|
+
os.makedirs(os.path.dirname(xw.USER_CONFIG_FILE), exist_ok=True)
|
428
|
+
settings = []
|
429
|
+
conda_path, conda_env = get_conda_settings()
|
430
|
+
if conda_path and sys.platform.startswith("win"):
|
431
|
+
settings.append('"CONDA PATH","{}"\n'.format(conda_path))
|
432
|
+
settings.append('"CONDA ENV","{}"\n'.format(conda_env))
|
433
|
+
else:
|
434
|
+
extension = "MAC" if sys.platform.startswith("darwin") else "WIN"
|
435
|
+
settings.append('"INTERPRETER_{}","{}"\n'.format(extension, sys.executable))
|
436
|
+
if not os.path.exists(xw.USER_CONFIG_FILE) or force:
|
437
|
+
with open(xw.USER_CONFIG_FILE, "w") as f:
|
438
|
+
f.writelines(settings)
|
439
|
+
|
440
|
+
|
441
|
+
def code_embed(args):
|
442
|
+
"""Import a specific file or all Python files of the Excel books' directory
|
443
|
+
into the active Excel Book
|
444
|
+
"""
|
445
|
+
wb = xw.books.active
|
446
|
+
single_file = False
|
447
|
+
if args and args.file:
|
448
|
+
source_files = [Path(args.file)]
|
449
|
+
single_file = True
|
450
|
+
else:
|
451
|
+
source_files = list(Path(wb.fullname).resolve().parent.rglob("*.py"))
|
452
|
+
if not source_files:
|
453
|
+
print("WARNING: Couldn't find any Python files in the workbook's directory!")
|
454
|
+
|
455
|
+
# Delete existing source code sheets
|
456
|
+
# A bug prevents deleting sheets from the collection directly (#1400)
|
457
|
+
with wb.app.properties(screen_updating=False):
|
458
|
+
if not single_file:
|
459
|
+
for sheetname in [sheet.name for sheet in wb.sheets]:
|
460
|
+
if wb.sheets[sheetname].name.endswith(".py"):
|
461
|
+
wb.sheets[sheetname].delete()
|
462
|
+
|
463
|
+
# Import source code
|
464
|
+
sheetname_to_path = {}
|
465
|
+
for source_file in source_files:
|
466
|
+
if not single_file:
|
467
|
+
sheetname = uuid.uuid4().hex[:28] + ".py"
|
468
|
+
sheetname_to_path[sheetname] = str(
|
469
|
+
source_file.relative_to(Path(wb.fullname).parent)
|
470
|
+
)
|
471
|
+
with open(source_file, "r", encoding="utf-8") as f:
|
472
|
+
content = []
|
473
|
+
for line in f.read().splitlines():
|
474
|
+
# Handle single-quote docstrings
|
475
|
+
line = line.replace("'''", '"""')
|
476
|
+
# Duplicate leading single quotes so Excel interprets them properly
|
477
|
+
# This is required even if the cell is in Text format
|
478
|
+
content.append(["'" + line if line.startswith("'") else line])
|
479
|
+
if single_file and source_file.name not in wb.sheet_names:
|
480
|
+
sheet = wb.sheets.add(
|
481
|
+
source_file.name, after=wb.sheets[len(wb.sheets) - 1]
|
482
|
+
)
|
483
|
+
elif single_file:
|
484
|
+
sheet = wb.sheets[source_file.name]
|
485
|
+
else:
|
486
|
+
sheet = wb.sheets.add(sheetname, after=wb.sheets[len(wb.sheets) - 1])
|
487
|
+
|
488
|
+
sheet["A1"].resize(
|
489
|
+
row_size=len(content) if content else 1
|
490
|
+
).number_format = "@"
|
491
|
+
sheet["A1"].value = content
|
492
|
+
sheet["A:A"].column_width = 65
|
493
|
+
|
494
|
+
# Update config with the sheetname_to_path mapping
|
495
|
+
if "xlwings.conf" in wb.sheet_names and not single_file:
|
496
|
+
config = xw.utils.read_config_sheet(wb)
|
497
|
+
sheetname_to_path_str = json.dumps(sheetname_to_path)
|
498
|
+
if len(sheetname_to_path_str) > 32_767:
|
499
|
+
sys.exit("ERROR: The package structure is too complex to embed.")
|
500
|
+
config["RELEASE_EMBED_CODE_MAP"] = sheetname_to_path_str
|
501
|
+
wb.sheets["xlwings.conf"]["A1"].value = config
|
502
|
+
elif not single_file:
|
503
|
+
config_sheet = wb.sheets.add(
|
504
|
+
"xlwings.conf", after=wb.sheets[len(wb.sheets) - 1]
|
505
|
+
)
|
506
|
+
config_sheet["A1"].value = {
|
507
|
+
"RELEASE_EMBED_CODE_MAP": json.dumps(sheetname_to_path)
|
508
|
+
}
|
509
|
+
|
510
|
+
|
511
|
+
def copy_os(args):
|
512
|
+
copy_code(Path(this_dir) / "js" / "xlwings.ts")
|
513
|
+
|
514
|
+
|
515
|
+
def copy_gs(args):
|
516
|
+
copy_code(Path(this_dir) / "js" / "xlwings.js")
|
517
|
+
|
518
|
+
|
519
|
+
def copy_vba(args):
|
520
|
+
if args.addin:
|
521
|
+
copy_code(Path(this_dir) / "xlwings_custom_addin.bas")
|
522
|
+
else:
|
523
|
+
copy_code(Path(this_dir) / "xlwings.bas")
|
524
|
+
|
525
|
+
|
526
|
+
def copy_customaddin(args):
|
527
|
+
copy_code(Path(this_dir) / "xlwings_custom_addin.bas")
|
528
|
+
|
529
|
+
|
530
|
+
def copy_code(fpath):
|
531
|
+
try:
|
532
|
+
from pandas.io import clipboard
|
533
|
+
except ImportError:
|
534
|
+
try:
|
535
|
+
import pyperclip as clipboard
|
536
|
+
except ImportError:
|
537
|
+
sys.exit(
|
538
|
+
'Please install either "pandas" or "pyperclip" to use the copy command.'
|
539
|
+
)
|
540
|
+
|
541
|
+
with open(fpath, "r", encoding="utf-8") as f:
|
542
|
+
if "bas" in str(fpath):
|
543
|
+
text = (
|
544
|
+
f.read()
|
545
|
+
.replace('Attribute VB_Name = "xlwings"\n', "")
|
546
|
+
.replace('Attribute VB_Name = "xlwings"\r\n', "")
|
547
|
+
)
|
548
|
+
else:
|
549
|
+
text = f.read()
|
550
|
+
clipboard.copy(text)
|
551
|
+
print("Successfully copied to clipboard.")
|
552
|
+
|
553
|
+
|
554
|
+
def import_remote_modules(book):
|
555
|
+
for vba_module in [
|
556
|
+
"IWebAuthenticator.cls",
|
557
|
+
"WebClient.cls",
|
558
|
+
"WebRequest.cls",
|
559
|
+
"WebResponse.cls",
|
560
|
+
"WebHelpers.bas",
|
561
|
+
]:
|
562
|
+
book.api.VBProject.VBComponents.Import(this_dir / "addin" / vba_module)
|
563
|
+
|
564
|
+
|
565
|
+
def release(args):
|
566
|
+
from tdrpa._tdxlwings.pro import LicenseHandler
|
567
|
+
from tdrpa._tdxlwings.utils import query_yes_no, read_user_config
|
568
|
+
|
569
|
+
if sys.platform.startswith("darwin"):
|
570
|
+
sys.exit(
|
571
|
+
"This command is currently only supported on Windows. "
|
572
|
+
"However, a released workbook will work on macOS, too."
|
573
|
+
)
|
574
|
+
|
575
|
+
if xw.apps:
|
576
|
+
book = xw.apps.active.books.active
|
577
|
+
else:
|
578
|
+
sys.exit("Please open your Excel file first.")
|
579
|
+
|
580
|
+
# Deploy Key
|
581
|
+
try:
|
582
|
+
deploy_key = LicenseHandler.create_deploy_key()
|
583
|
+
except xw.LicenseError:
|
584
|
+
# Can't create deploy key with trial keys, so use trial key directly
|
585
|
+
deploy_key = read_user_config()["license_key"]
|
586
|
+
|
587
|
+
# Sheet Config
|
588
|
+
if "xlwings.conf" not in [i.name for i in book.sheets]:
|
589
|
+
project_name = input("Name of your one-click installer? ")
|
590
|
+
use_embedded_code = query_yes_no("Embed your Python code?")
|
591
|
+
hide_config_sheet = query_yes_no("Hide the config sheet?")
|
592
|
+
if use_embedded_code:
|
593
|
+
hide_code_sheets = query_yes_no(
|
594
|
+
"Hide the sheets with the embedded Python code?"
|
595
|
+
)
|
596
|
+
else:
|
597
|
+
hide_code_sheets = False
|
598
|
+
use_without_addin = query_yes_no(
|
599
|
+
"Allow your tool to run without the xlwings add-in?"
|
600
|
+
)
|
601
|
+
use_remote = query_yes_no("Support remote interpreter?", "no")
|
602
|
+
print()
|
603
|
+
if not query_yes_no(f'This will release "{book.name}", proceed?'):
|
604
|
+
sys.exit()
|
605
|
+
else:
|
606
|
+
print()
|
607
|
+
if "_xlwings.conf" in [sheet.name for sheet in book.sheets]:
|
608
|
+
print("* Remove _xlwings.conf sheet")
|
609
|
+
book.sheets["_xlwings.conf"].delete()
|
610
|
+
active_sheet = book.sheets.active
|
611
|
+
print("* Add xlwings.conf sheet")
|
612
|
+
config_sheet = book.sheets.add(
|
613
|
+
"xlwings.conf", after=book.sheets[len(book.sheets) - 1]
|
614
|
+
)
|
615
|
+
active_sheet.activate() # preserve the currently active sheet
|
616
|
+
config = {
|
617
|
+
"Interpreter_Win": r"%LOCALAPPDATA%\{0}\python.exe".format(project_name)
|
618
|
+
if project_name
|
619
|
+
else None,
|
620
|
+
"Interpreter_Mac": f"$HOME/{project_name}/bin/python"
|
621
|
+
if project_name
|
622
|
+
else None,
|
623
|
+
"PYTHONPATH": None,
|
624
|
+
"Conda Path": None,
|
625
|
+
"Conda Env": None,
|
626
|
+
"UDF Modules": None,
|
627
|
+
"Debug UDFs": False,
|
628
|
+
"Use UDF Server": False,
|
629
|
+
"Show Console": False,
|
630
|
+
"LICENSE_KEY": deploy_key,
|
631
|
+
"RELEASE_EMBED_CODE": use_embedded_code,
|
632
|
+
"RELEASE_HIDE_CONFIG_SHEET": hide_config_sheet,
|
633
|
+
"RELEASE_HIDE_CODE_SHEETS": hide_code_sheets,
|
634
|
+
"RELEASE_NO_ADDIN": use_without_addin,
|
635
|
+
"RELEASE_REMOTE_INTERPRETER": use_remote,
|
636
|
+
}
|
637
|
+
config_sheet["A1"].value = config
|
638
|
+
config_sheet["A:A"].autofit()
|
639
|
+
else:
|
640
|
+
print()
|
641
|
+
if not query_yes_no(
|
642
|
+
f'This will release "{book.name}" '
|
643
|
+
f'according to the "xlwings.conf" sheet, proceed?'
|
644
|
+
):
|
645
|
+
sys.exit()
|
646
|
+
print()
|
647
|
+
# Only update the deploy key
|
648
|
+
config = xw.utils.read_config_sheet(book)
|
649
|
+
print("* Update deploy key")
|
650
|
+
config["LICENSE_KEY"] = deploy_key
|
651
|
+
book.sheets["xlwings.conf"]["A1"].value = config
|
652
|
+
|
653
|
+
# Remove Reference
|
654
|
+
if config["RELEASE_NO_ADDIN"]:
|
655
|
+
if "xlwings" in [i.Name for i in book.api.VBProject.References]:
|
656
|
+
print("* Remove VBA Reference")
|
657
|
+
ref = book.api.VBProject.References("xlwings")
|
658
|
+
book.api.VBProject.References.Remove(ref)
|
659
|
+
|
660
|
+
# Remove VBA modules/classes
|
661
|
+
print("* Update VBA modules")
|
662
|
+
|
663
|
+
for vba_module in [
|
664
|
+
"xlwings",
|
665
|
+
"Dictionary",
|
666
|
+
"IWebAuthenticator",
|
667
|
+
"WebClient",
|
668
|
+
"WebRequest",
|
669
|
+
"WebResponse",
|
670
|
+
"WebHelpers",
|
671
|
+
]:
|
672
|
+
if vba_module in [i.Name for i in book.api.VBProject.VBComponents]:
|
673
|
+
book.api.VBProject.VBComponents.Remove(
|
674
|
+
book.api.VBProject.VBComponents(vba_module)
|
675
|
+
)
|
676
|
+
|
677
|
+
# Import VBA modules/classes
|
678
|
+
book.api.VBProject.VBComponents.Import(this_dir / "xlwings.bas")
|
679
|
+
book.api.VBProject.VBComponents.Import(this_dir / "addin" / "Dictionary.cls")
|
680
|
+
if config["RELEASE_REMOTE_INTERPRETER"]:
|
681
|
+
import_remote_modules(book)
|
682
|
+
|
683
|
+
# Embed code
|
684
|
+
if config.get("RELEASE_EMBED_CODE"):
|
685
|
+
print("* Embed Python code")
|
686
|
+
code_embed(None)
|
687
|
+
|
688
|
+
# Hide sheets
|
689
|
+
if config.get("RELEASE_HIDE_CONFIG_SHEET"):
|
690
|
+
print("* Hide config sheet")
|
691
|
+
book.sheets["xlwings.conf"].visible = False
|
692
|
+
|
693
|
+
if config.get("RELEASE_HIDE_CODE_SHEETS"):
|
694
|
+
print("* Hide Python sheets")
|
695
|
+
for sheet in book.sheets:
|
696
|
+
if sheet.name.endswith(".py"):
|
697
|
+
sheet.visible = False
|
698
|
+
print()
|
699
|
+
print(
|
700
|
+
"Checking for xlwings version compatibility "
|
701
|
+
"between the one-click installer and the Excel file..."
|
702
|
+
)
|
703
|
+
if sys.platform.startswith("win") and config["Interpreter_Win"]:
|
704
|
+
interpreter_path = os.path.expandvars(config["Interpreter_Win"])
|
705
|
+
elif sys.platform.startswith("darwin") and config["Interpreter_Mac"]:
|
706
|
+
interpreter_path = os.path.expandvars(config["Interpreter_Mac"])
|
707
|
+
else:
|
708
|
+
interpreter_path = None
|
709
|
+
if interpreter_path and Path(interpreter_path).is_file():
|
710
|
+
res = subprocess.run(
|
711
|
+
[
|
712
|
+
interpreter_path,
|
713
|
+
"-c",
|
714
|
+
"import warnings;warnings.filterwarnings('ignore');"
|
715
|
+
"import tdrpa._tdxlwings as xlwings;print(xlwings.__version__)",
|
716
|
+
],
|
717
|
+
stdout=subprocess.PIPE,
|
718
|
+
stderr=subprocess.STDOUT,
|
719
|
+
encoding="utf-8",
|
720
|
+
)
|
721
|
+
xlwings_version_installer = res.stdout.strip()
|
722
|
+
if xlwings_version_installer == xw.__version__:
|
723
|
+
print(f'Successfully prepared "{book.name}" for release!')
|
724
|
+
else:
|
725
|
+
print(
|
726
|
+
f"ERROR: You are running this command with xlwings {xw.__version__} "
|
727
|
+
f"but your installer uses {xlwings_version_installer}!"
|
728
|
+
)
|
729
|
+
else:
|
730
|
+
print(
|
731
|
+
f"WARNING: Prepared '{book.name}' for release "
|
732
|
+
"but couldn't verify the xlwings version!"
|
733
|
+
)
|
734
|
+
|
735
|
+
|
736
|
+
def export_vba_modules(book, overwrite=False):
|
737
|
+
# TODO: catch error when Trust Access to VBA Object model isn't enabled
|
738
|
+
# TODO: raise error if editing while file hashes differ
|
739
|
+
type_to_ext = {100: "cls", 1: "bas", 2: "cls", 3: "frm"}
|
740
|
+
path_to_type = {}
|
741
|
+
for vb_component in book.api.VBProject.VBComponents:
|
742
|
+
file_path = (
|
743
|
+
Path(".").resolve()
|
744
|
+
/ f"{vb_component.Name}.{type_to_ext[vb_component.Type]}"
|
745
|
+
)
|
746
|
+
path_to_type[str(file_path)] = vb_component.Type
|
747
|
+
if (
|
748
|
+
vb_component.Type == 100 and vb_component.CodeModule.CountOfLines > 0
|
749
|
+
) or vb_component.Type != 100:
|
750
|
+
# Prevents cluttering everything with empty files if you have lots of sheets
|
751
|
+
if overwrite or not file_path.exists():
|
752
|
+
vb_component.Export(str(file_path))
|
753
|
+
if vb_component.Type == 100:
|
754
|
+
# Remove the meta info so it can be distinguished from regular
|
755
|
+
# classes when running "xlwings vba import"
|
756
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
757
|
+
exported_code = f.readlines()
|
758
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
759
|
+
f.writelines(exported_code[9:])
|
760
|
+
return path_to_type
|
761
|
+
|
762
|
+
|
763
|
+
def vba_get_book(args):
|
764
|
+
import textwrap
|
765
|
+
|
766
|
+
from tdrpa._tdxlwings.utils import query_yes_no
|
767
|
+
|
768
|
+
if args and args.file:
|
769
|
+
book = xw.Book(args.file)
|
770
|
+
else:
|
771
|
+
if not xw.apps:
|
772
|
+
sys.exit(
|
773
|
+
"Your workbook must be open or you have to supply the --file argument."
|
774
|
+
)
|
775
|
+
else:
|
776
|
+
book = xw.books.active
|
777
|
+
|
778
|
+
tf = query_yes_no(
|
779
|
+
textwrap.dedent(
|
780
|
+
f"""
|
781
|
+
This will affect the following workbook/folder:
|
782
|
+
|
783
|
+
* {book.name}
|
784
|
+
* {Path(".").resolve()}
|
785
|
+
|
786
|
+
Proceed?"""
|
787
|
+
)
|
788
|
+
)
|
789
|
+
|
790
|
+
if not tf:
|
791
|
+
sys.exit()
|
792
|
+
return book
|
793
|
+
|
794
|
+
|
795
|
+
def vba_import(args):
|
796
|
+
exit_unsupported_platform()
|
797
|
+
import pywintypes
|
798
|
+
|
799
|
+
book = vba_get_book(args)
|
800
|
+
|
801
|
+
for path in Path(".").resolve().glob("*"):
|
802
|
+
if path.suffix == ".bas":
|
803
|
+
try:
|
804
|
+
vb_component = book.api.VBProject.VBComponents(path.stem)
|
805
|
+
book.api.VBProject.VBComponents.Remove(vb_component)
|
806
|
+
except pywintypes.com_error:
|
807
|
+
pass
|
808
|
+
book.api.VBProject.VBComponents.Import(path)
|
809
|
+
elif path.suffix in (".cls", ".frm"):
|
810
|
+
with open(path, "r", encoding="utf-8") as f:
|
811
|
+
vba_code = f.readlines()
|
812
|
+
if vba_code:
|
813
|
+
if vba_code[0].startswith("VERSION "):
|
814
|
+
# For frm, this also imports frx, unlike in editing mode
|
815
|
+
try:
|
816
|
+
vb_component = book.api.VBProject.VBComponents(path.stem)
|
817
|
+
book.api.VBProject.VBComponents.Remove(vb_component)
|
818
|
+
except pywintypes.com_error:
|
819
|
+
pass
|
820
|
+
book.api.VBProject.VBComponents.Import(path)
|
821
|
+
else:
|
822
|
+
vb_component = book.api.VBProject.VBComponents(path.stem)
|
823
|
+
line_count = vb_component.CodeModule.CountOfLines
|
824
|
+
if line_count > 0:
|
825
|
+
vb_component.CodeModule.DeleteLines(1, line_count)
|
826
|
+
vb_component.CodeModule.AddFromString("".join(vba_code))
|
827
|
+
book.save()
|
828
|
+
|
829
|
+
|
830
|
+
def vba_export(args):
|
831
|
+
exit_unsupported_platform()
|
832
|
+
book = vba_get_book(args)
|
833
|
+
export_vba_modules(book, overwrite=True)
|
834
|
+
print(f"Successfully exported the VBA modules from {book.name}!")
|
835
|
+
|
836
|
+
|
837
|
+
def vba_edit(args):
|
838
|
+
exit_unsupported_platform()
|
839
|
+
import pywintypes
|
840
|
+
|
841
|
+
try:
|
842
|
+
from watchgod import Change, RegExpWatcher, watch
|
843
|
+
except ImportError:
|
844
|
+
sys.exit(
|
845
|
+
"Please install watchgod to use this functionality: pip install watchgod"
|
846
|
+
)
|
847
|
+
|
848
|
+
book = vba_get_book(args)
|
849
|
+
|
850
|
+
path_to_type = export_vba_modules(book, overwrite=False)
|
851
|
+
mode = "verbose" if args.verbose else "silent"
|
852
|
+
|
853
|
+
print("NOTE: Deleting a VBA module here will also delete it in the VBA editor!")
|
854
|
+
print(f"Watching for changes in {book.name} ({mode} mode)...(Hit Ctrl-C to stop)")
|
855
|
+
|
856
|
+
for changes in watch(
|
857
|
+
Path(".").resolve(),
|
858
|
+
watcher_cls=RegExpWatcher,
|
859
|
+
watcher_kwargs=dict(re_files=r"^.*(\.cls|\.frm|\.bas)$"),
|
860
|
+
normal_sleep=400,
|
861
|
+
):
|
862
|
+
for change_type, path in changes:
|
863
|
+
module_name = os.path.splitext(os.path.basename(path))[0]
|
864
|
+
module_type = path_to_type[path]
|
865
|
+
vb_component = book.api.VBProject.VBComponents(module_name)
|
866
|
+
if change_type == Change.modified:
|
867
|
+
with open(path, "r", encoding="utf-8") as f:
|
868
|
+
vba_code = f.readlines()
|
869
|
+
line_count = vb_component.CodeModule.CountOfLines
|
870
|
+
if line_count > 0:
|
871
|
+
vb_component.CodeModule.DeleteLines(1, line_count)
|
872
|
+
# ThisWorkbook/Sheet, bas, cls, frm
|
873
|
+
type_to_firstline = {100: 0, 1: 1, 2: 9, 3: 15}
|
874
|
+
try:
|
875
|
+
vb_component.CodeModule.AddFromString(
|
876
|
+
"".join(vba_code[type_to_firstline[module_type] :])
|
877
|
+
)
|
878
|
+
except pywintypes.com_error:
|
879
|
+
print(
|
880
|
+
f"ERROR: Couldn't update module {module_name}. "
|
881
|
+
f"Please update changes manually."
|
882
|
+
)
|
883
|
+
if args.verbose:
|
884
|
+
print(f"INFO: Updated module {module_name}.")
|
885
|
+
elif change_type == Change.deleted:
|
886
|
+
try:
|
887
|
+
book.api.VBProject.VBComponents.Remove(vb_component)
|
888
|
+
except pywintypes.com_error:
|
889
|
+
print(
|
890
|
+
f"ERROR: Couldn't delete module {module_name}. "
|
891
|
+
f"Please delete it manually."
|
892
|
+
)
|
893
|
+
elif change_type == Change.added:
|
894
|
+
print(
|
895
|
+
f"ERROR: Couldn't add {module_name} as this isn't supported. "
|
896
|
+
"Please add new files via the VBA Editor."
|
897
|
+
)
|
898
|
+
book.save()
|
899
|
+
|
900
|
+
|
901
|
+
def py_edit(args):
|
902
|
+
try:
|
903
|
+
from watchgod import RegExpWatcher, watch
|
904
|
+
except ImportError:
|
905
|
+
sys.exit(
|
906
|
+
"Please install watchgod to use this functionality: pip install watchgod"
|
907
|
+
)
|
908
|
+
book = xw.books.active
|
909
|
+
selection = book.selection
|
910
|
+
source_path = (
|
911
|
+
selection.get_address(include_sheetname=True, external=True)
|
912
|
+
.replace("!", "")
|
913
|
+
.replace(" ", "_")
|
914
|
+
.replace("'", "")
|
915
|
+
.replace("[", "")
|
916
|
+
.replace("]", "_")
|
917
|
+
+ ".py"
|
918
|
+
)
|
919
|
+
|
920
|
+
Path(source_path).write_text(selection.formula.strip()[5:-4].replace('""', '"'))
|
921
|
+
print(f"Open {Path(source_path).resolve()} to edit!")
|
922
|
+
print("Syncing changes... (Hit Ctrl-C to stop)")
|
923
|
+
for changes in watch(
|
924
|
+
Path(".").resolve(),
|
925
|
+
watcher_cls=RegExpWatcher,
|
926
|
+
watcher_kwargs=dict(re_files=r"^.*(\.py)$"),
|
927
|
+
normal_sleep=400,
|
928
|
+
):
|
929
|
+
source_code = Path(source_path).read_text()
|
930
|
+
# 1 = Object, 0 = Value
|
931
|
+
# Note that the initial beta version only supports 1
|
932
|
+
selection.value = '=PY("{0}",1)'.format(source_code.replace('"', '""'))
|
933
|
+
|
934
|
+
|
935
|
+
def main():
|
936
|
+
print("xlwings version: " + xw.__version__)
|
937
|
+
parser = argparse.ArgumentParser()
|
938
|
+
subparsers = parser.add_subparsers(dest="command")
|
939
|
+
subparsers.required = True
|
940
|
+
|
941
|
+
# Add-in
|
942
|
+
addin_parser = subparsers.add_parser(
|
943
|
+
"addin",
|
944
|
+
help='Run "xlwings addin install" to install the Excel add-in '
|
945
|
+
'(will be copied to the user\'s XLSTART folder). Instead of "install" you can '
|
946
|
+
'also use "update", "remove" or "status". Note that this command '
|
947
|
+
"may take a while. You can install your custom add-in "
|
948
|
+
"by providing the name or path via the --file/-f flag, "
|
949
|
+
'e.g. "xlwings add-in install -f custom.xlam or copy all Excel '
|
950
|
+
"files in a directory to the XLSTART folder by providing the path "
|
951
|
+
'via the --dir flag." To install the add-in for every user globally, use the '
|
952
|
+
" --glob/-g flag and run this command from an Elevated Command Prompt.",
|
953
|
+
)
|
954
|
+
addin_subparsers = addin_parser.add_subparsers(dest="subcommand")
|
955
|
+
addin_subparsers.required = True
|
956
|
+
|
957
|
+
file_arg_help = "The name or path of a custom add-in."
|
958
|
+
dir_arg_help = (
|
959
|
+
"The path of a directory whose Excel files you want to copy to or remove from "
|
960
|
+
"XLSTART."
|
961
|
+
)
|
962
|
+
glob_arg_help = "Install the add-in for all users."
|
963
|
+
|
964
|
+
addin_install_parser = addin_subparsers.add_parser("install")
|
965
|
+
|
966
|
+
addin_install_parser.add_argument("-f", "--file", default=None, help=file_arg_help)
|
967
|
+
addin_install_parser.add_argument("-d", "--dir", default=None, help=dir_arg_help)
|
968
|
+
addin_install_parser.add_argument(
|
969
|
+
"-g", "--glob", action="store_true", help=glob_arg_help
|
970
|
+
)
|
971
|
+
addin_install_parser.set_defaults(func=addin_install)
|
972
|
+
|
973
|
+
addin_update_parser = addin_subparsers.add_parser("update")
|
974
|
+
addin_update_parser.add_argument("-f", "--file", default=None, help=file_arg_help)
|
975
|
+
addin_update_parser.add_argument("-d", "--dir", default=None, help=dir_arg_help)
|
976
|
+
addin_update_parser.add_argument(
|
977
|
+
"-g", "--glob", action="store_true", help=glob_arg_help
|
978
|
+
)
|
979
|
+
addin_update_parser.set_defaults(func=addin_install)
|
980
|
+
|
981
|
+
addin_upgrade_parser = addin_subparsers.add_parser("upgrade")
|
982
|
+
addin_upgrade_parser.add_argument("-f", "--file", default=None, help=file_arg_help)
|
983
|
+
addin_upgrade_parser.add_argument("-d", "--dir", default=None, help=dir_arg_help)
|
984
|
+
addin_upgrade_parser.add_argument(
|
985
|
+
"-g", "--glob", action="store_true", help=glob_arg_help
|
986
|
+
)
|
987
|
+
addin_upgrade_parser.set_defaults(func=addin_install)
|
988
|
+
|
989
|
+
addin_remove_parser = addin_subparsers.add_parser("remove")
|
990
|
+
addin_remove_parser.add_argument("-f", "--file", default=None, help=file_arg_help)
|
991
|
+
addin_remove_parser.add_argument("-d", "--dir", default=None, help=dir_arg_help)
|
992
|
+
addin_remove_parser.add_argument(
|
993
|
+
"-g", "--glob", action="store_true", help=glob_arg_help
|
994
|
+
)
|
995
|
+
addin_remove_parser.set_defaults(func=addin_remove)
|
996
|
+
|
997
|
+
addin_uninstall_parser = addin_subparsers.add_parser("uninstall")
|
998
|
+
addin_uninstall_parser.add_argument(
|
999
|
+
"-f", "--file", default=None, help=file_arg_help
|
1000
|
+
)
|
1001
|
+
addin_uninstall_parser.add_argument("-d", "--dir", default=None, help=dir_arg_help)
|
1002
|
+
addin_uninstall_parser.add_argument(
|
1003
|
+
"-g", "--glob", action="store_true", help=glob_arg_help
|
1004
|
+
)
|
1005
|
+
addin_uninstall_parser.set_defaults(func=addin_remove)
|
1006
|
+
|
1007
|
+
addin_status_parser = addin_subparsers.add_parser("status")
|
1008
|
+
addin_status_parser.add_argument("-f", "--file", default=None, help=file_arg_help)
|
1009
|
+
addin_status_parser.add_argument(
|
1010
|
+
"-g", "--glob", action="store_true", help=glob_arg_help
|
1011
|
+
)
|
1012
|
+
addin_status_parser.set_defaults(func=addin_status)
|
1013
|
+
|
1014
|
+
# Quickstart
|
1015
|
+
quickstart_parser = subparsers.add_parser(
|
1016
|
+
"quickstart",
|
1017
|
+
help='Run "xlwings quickstart myproject" to create a '
|
1018
|
+
'folder called "myproject" in the current directory '
|
1019
|
+
"with an Excel file and a Python file, ready to be "
|
1020
|
+
'used. Use the "--standalone" flag to embed all VBA '
|
1021
|
+
"code in the Excel file and make it work without the "
|
1022
|
+
"xlwings add-in. "
|
1023
|
+
'Use "--fastapi" for creating a project that uses a remote '
|
1024
|
+
"Python interpreter. "
|
1025
|
+
'Use "--addin --ribbon" to create a template for a custom ribbon addin. Leave '
|
1026
|
+
'away the "--ribbon" if you don\'t want a ribbon tab. ',
|
1027
|
+
)
|
1028
|
+
quickstart_parser.add_argument("project_name")
|
1029
|
+
quickstart_parser.add_argument(
|
1030
|
+
"-s", "--standalone", action="store_true", help="Include xlwings as VBA module."
|
1031
|
+
)
|
1032
|
+
quickstart_parser.add_argument(
|
1033
|
+
"-r",
|
1034
|
+
"--remote",
|
1035
|
+
action="store_true",
|
1036
|
+
help="Support a remote Python interpreter.",
|
1037
|
+
)
|
1038
|
+
quickstart_parser.add_argument(
|
1039
|
+
"-fastapi",
|
1040
|
+
"--fastapi",
|
1041
|
+
action="store_true",
|
1042
|
+
help="Create a FastAPI project suitable for a remote Python interpreter.",
|
1043
|
+
)
|
1044
|
+
quickstart_parser.add_argument(
|
1045
|
+
"-addin", "--addin", action="store_true", help="Create an add-in."
|
1046
|
+
)
|
1047
|
+
quickstart_parser.add_argument(
|
1048
|
+
"-ribbon",
|
1049
|
+
"--ribbon",
|
1050
|
+
action="store_true",
|
1051
|
+
help="Include a ribbon when creating an add-in.",
|
1052
|
+
)
|
1053
|
+
quickstart_parser.set_defaults(func=quickstart)
|
1054
|
+
|
1055
|
+
# RunPython (macOS only)
|
1056
|
+
if sys.platform.startswith("darwin"):
|
1057
|
+
runpython_parser = subparsers.add_parser(
|
1058
|
+
"runpython",
|
1059
|
+
help='macOS only: run "xlwings runpython install" if you '
|
1060
|
+
"want to enable the RunPython calls without installing "
|
1061
|
+
"the add-in. This will create the following file: "
|
1062
|
+
"~/Library/Application Scripts/com.microsoft.Excel/"
|
1063
|
+
"xlwings-x.x.x.applescript",
|
1064
|
+
)
|
1065
|
+
runpython_subparser = runpython_parser.add_subparsers(dest="subcommand")
|
1066
|
+
runpython_subparser.required = True
|
1067
|
+
|
1068
|
+
runpython_install_parser = runpython_subparser.add_parser("install")
|
1069
|
+
runpython_install_parser.set_defaults(func=runpython_install)
|
1070
|
+
|
1071
|
+
# restapi run
|
1072
|
+
restapi_parser = subparsers.add_parser(
|
1073
|
+
"restapi",
|
1074
|
+
help='Use "xlwings restapi run" to run the xlwings REST API via Flask '
|
1075
|
+
'development server. Accepts "--host" and "--port" as optional arguments.',
|
1076
|
+
)
|
1077
|
+
restapi_subparser = restapi_parser.add_subparsers(dest="subcommand")
|
1078
|
+
restapi_subparser.required = True
|
1079
|
+
|
1080
|
+
restapi_run_parser = restapi_subparser.add_parser("run")
|
1081
|
+
restapi_run_parser.add_argument(
|
1082
|
+
"-host", "--host", default="127.0.0.1", help="The interface to bind to."
|
1083
|
+
)
|
1084
|
+
restapi_run_parser.add_argument(
|
1085
|
+
"-p", "--port", default="5000", help="The port to bind to."
|
1086
|
+
)
|
1087
|
+
restapi_run_parser.set_defaults(func=restapi_run)
|
1088
|
+
|
1089
|
+
# License
|
1090
|
+
license_parser = subparsers.add_parser(
|
1091
|
+
"license",
|
1092
|
+
help='xlwings PRO: Use "xlwings license update -k KEY" where '
|
1093
|
+
'"KEY" is your personal (trial) license key. This will '
|
1094
|
+
"update ~/.xlwings/xlwings.conf with the LICENSE_KEY entry. "
|
1095
|
+
'If you have a paid license, you can run "xlwings license deploy" '
|
1096
|
+
"to create a deploy key. This is not available for trial keys.",
|
1097
|
+
)
|
1098
|
+
license_subparsers = license_parser.add_subparsers(dest="subcommand")
|
1099
|
+
license_subparsers.required = True
|
1100
|
+
|
1101
|
+
license_update_parser = license_subparsers.add_parser("update")
|
1102
|
+
license_update_parser.add_argument(
|
1103
|
+
"-k", "--key", help="Updates the LICENSE_KEY in ~/.xlwings/xlwings.conf."
|
1104
|
+
)
|
1105
|
+
license_update_parser.set_defaults(func=license_update)
|
1106
|
+
|
1107
|
+
license_update_parser = license_subparsers.add_parser("deploy")
|
1108
|
+
license_update_parser.set_defaults(func=license_deploy)
|
1109
|
+
|
1110
|
+
# Config
|
1111
|
+
config_parser = subparsers.add_parser(
|
1112
|
+
"config",
|
1113
|
+
help='Run "xlwings config create" to create the user config file '
|
1114
|
+
"(~/.xlwings/xlwings.conf) which is where the settings from "
|
1115
|
+
"the Ribbon add-in are stored. It will configure the Python "
|
1116
|
+
"interpreter that you are running this command with. To reset "
|
1117
|
+
'your configuration, run this with the "--force" flag which '
|
1118
|
+
"will overwrite your current configuration.",
|
1119
|
+
)
|
1120
|
+
config_subparsers = config_parser.add_subparsers(dest="subcommand")
|
1121
|
+
config_subparsers.required = True
|
1122
|
+
|
1123
|
+
config_create_parser = config_subparsers.add_parser("create")
|
1124
|
+
config_create_parser.add_argument(
|
1125
|
+
"-f",
|
1126
|
+
"--force",
|
1127
|
+
action="store_true",
|
1128
|
+
help="Will overwrite the current config file.",
|
1129
|
+
)
|
1130
|
+
config_create_parser.set_defaults(func=config_create)
|
1131
|
+
|
1132
|
+
# Embed code
|
1133
|
+
code_parser = subparsers.add_parser(
|
1134
|
+
"code",
|
1135
|
+
help='Run "xlwings code embed" to embed all Python modules of the '
|
1136
|
+
"""workbook's dir in your active Excel file. Use the "--file/-f" flag to """
|
1137
|
+
"only import a single file by providing its path. Requires "
|
1138
|
+
"xlwings PRO.",
|
1139
|
+
)
|
1140
|
+
code_subparsers = code_parser.add_subparsers(dest="subcommand")
|
1141
|
+
code_subparsers.required = True
|
1142
|
+
|
1143
|
+
code_create_parser = code_subparsers.add_parser("embed")
|
1144
|
+
code_create_parser.add_argument(
|
1145
|
+
"-f",
|
1146
|
+
"--file",
|
1147
|
+
help="Optional parameter to only import a single file provided as file path.",
|
1148
|
+
)
|
1149
|
+
code_create_parser.set_defaults(func=code_embed)
|
1150
|
+
|
1151
|
+
# Release
|
1152
|
+
release_parser = subparsers.add_parser(
|
1153
|
+
"release",
|
1154
|
+
help='Run "xlwings release" to configure your active workbook to work with a '
|
1155
|
+
"one-click installer for easy deployment. Requires xlwings PRO.",
|
1156
|
+
)
|
1157
|
+
release_parser.set_defaults(func=release)
|
1158
|
+
|
1159
|
+
# Copy
|
1160
|
+
copy_parser = subparsers.add_parser(
|
1161
|
+
"copy",
|
1162
|
+
help='Run "xlwings copy os" to copy the xlwings Office Scripts module. '
|
1163
|
+
'Run "xlwings copy gs" to copy the xlwings Google Apps Script module. '
|
1164
|
+
'Run "xlwings copy vba" to copy the standalone xlwings VBA module. '
|
1165
|
+
'Run "xlwings copy vba --addin" to copy the xlwings VBA module for custom '
|
1166
|
+
"add-ins.",
|
1167
|
+
)
|
1168
|
+
copy_subparser = copy_parser.add_subparsers(dest="subcommand")
|
1169
|
+
copy_subparser.required = True
|
1170
|
+
|
1171
|
+
copy_os_parser = copy_subparser.add_parser("os")
|
1172
|
+
copy_os_parser.set_defaults(func=copy_os)
|
1173
|
+
|
1174
|
+
copy_os_parser = copy_subparser.add_parser("gs")
|
1175
|
+
copy_os_parser.set_defaults(func=copy_gs)
|
1176
|
+
|
1177
|
+
copy_vba_parser = copy_subparser.add_parser("vba")
|
1178
|
+
copy_vba_parser.add_argument(
|
1179
|
+
"-a", "--addin", action="store_true", help="VBA for custom add-ins"
|
1180
|
+
)
|
1181
|
+
copy_vba_parser.set_defaults(func=copy_vba)
|
1182
|
+
|
1183
|
+
# Azure AD authentication (MSAL)
|
1184
|
+
auth_parser = subparsers.add_parser(
|
1185
|
+
"auth",
|
1186
|
+
help='Microsoft Azure AD: "xlwings auth azuread", see '
|
1187
|
+
"https://docs.xlwings.org/en/stable/server_authentication.html",
|
1188
|
+
)
|
1189
|
+
|
1190
|
+
aad_subparser = auth_parser.add_subparsers(dest="subcommand")
|
1191
|
+
aad_subparser.required = True
|
1192
|
+
|
1193
|
+
auth_aad_parser = aad_subparser.add_parser("azuread")
|
1194
|
+
auth_aad_parser.set_defaults(func=auth_aad)
|
1195
|
+
auth_aad_parser.add_argument(
|
1196
|
+
"-tid",
|
1197
|
+
"--tenant_id",
|
1198
|
+
help="Tenant ID",
|
1199
|
+
)
|
1200
|
+
auth_aad_parser.add_argument(
|
1201
|
+
"-cid",
|
1202
|
+
"--client_id",
|
1203
|
+
help="CLIENT ID",
|
1204
|
+
)
|
1205
|
+
auth_aad_parser.add_argument(
|
1206
|
+
"-p",
|
1207
|
+
"--port",
|
1208
|
+
help="Port",
|
1209
|
+
)
|
1210
|
+
auth_aad_parser.add_argument(
|
1211
|
+
"-s",
|
1212
|
+
"--scopes",
|
1213
|
+
help="Scopes",
|
1214
|
+
)
|
1215
|
+
auth_aad_parser.add_argument(
|
1216
|
+
"-u",
|
1217
|
+
"--username",
|
1218
|
+
help="Username",
|
1219
|
+
)
|
1220
|
+
auth_aad_parser.add_argument(
|
1221
|
+
"-r", "--reset", action="store_true", help="Clear local cache."
|
1222
|
+
)
|
1223
|
+
# Edit VBA code
|
1224
|
+
vba_parser = subparsers.add_parser(
|
1225
|
+
"vba",
|
1226
|
+
help="""This functionality allows you to easily write VBA code in an external
|
1227
|
+
editor: run "xlwings vba edit" to update the VBA modules of the active workbook
|
1228
|
+
from their local exports everytime you hit save. If you run this the first time,
|
1229
|
+
the modules will be exported from Excel into your current working directory.
|
1230
|
+
To overwrite the local version of the modules with those from Excel,
|
1231
|
+
run "xlwings vba export". To overwrite the VBA modules in Excel with their local
|
1232
|
+
versions, run "xlwings vba import".
|
1233
|
+
The "--file/-f" flag allows you to specify a file path instead of using the
|
1234
|
+
active Workbook. Requires "Trust access to the VBA project object model"
|
1235
|
+
enabled.
|
1236
|
+
NOTE: Whenever you change something in the VBA editor (such as the layout of a
|
1237
|
+
form or the properties of a module), you have to run "xlwings vba export".
|
1238
|
+
""",
|
1239
|
+
)
|
1240
|
+
vba_subparsers = vba_parser.add_subparsers(dest="subcommand")
|
1241
|
+
vba_subparsers.required = True
|
1242
|
+
|
1243
|
+
vba_edit_parser = vba_subparsers.add_parser("edit")
|
1244
|
+
vba_edit_parser.add_argument(
|
1245
|
+
"-f",
|
1246
|
+
"--file",
|
1247
|
+
help="Optional parameter to select a specific workbook, otherwise it uses the "
|
1248
|
+
"active one.",
|
1249
|
+
)
|
1250
|
+
vba_edit_parser.add_argument(
|
1251
|
+
"-v",
|
1252
|
+
"--verbose",
|
1253
|
+
action="store_true",
|
1254
|
+
help="Optional parameter to print messages whenever a module has been updated "
|
1255
|
+
"successfully.",
|
1256
|
+
)
|
1257
|
+
|
1258
|
+
vba_edit_parser.set_defaults(func=vba_edit)
|
1259
|
+
|
1260
|
+
vba_export_parser = vba_subparsers.add_parser("export")
|
1261
|
+
vba_export_parser.add_argument(
|
1262
|
+
"-f",
|
1263
|
+
"--file",
|
1264
|
+
help="Optional parameter to select a specific file, otherwise it uses the "
|
1265
|
+
"active one.",
|
1266
|
+
)
|
1267
|
+
|
1268
|
+
vba_export_parser.set_defaults(func=vba_export)
|
1269
|
+
|
1270
|
+
vba_import_parser = vba_subparsers.add_parser("import")
|
1271
|
+
vba_import_parser.add_argument(
|
1272
|
+
"-f",
|
1273
|
+
"--file",
|
1274
|
+
help="Optional parameter to select a specific file, otherwise it uses the "
|
1275
|
+
"active one.",
|
1276
|
+
)
|
1277
|
+
|
1278
|
+
vba_import_parser.set_defaults(func=vba_import)
|
1279
|
+
|
1280
|
+
# Edit =PY cells
|
1281
|
+
py_parser = subparsers.add_parser(
|
1282
|
+
"py",
|
1283
|
+
help="""This functionality allows you to easily write Python code for
|
1284
|
+
Microsoft's Python in Excel cells (=PY) via a local editor: run "xlwings py edit" to
|
1285
|
+
export the code of the selected cell into a local file. Whenever you save the
|
1286
|
+
file, the code will be synced back to the cell.
|
1287
|
+
""",
|
1288
|
+
)
|
1289
|
+
py_subparsers = py_parser.add_subparsers(dest="subcommand")
|
1290
|
+
py_subparsers.required = True
|
1291
|
+
|
1292
|
+
py_edit_parser = py_subparsers.add_parser("edit")
|
1293
|
+
py_edit_parser.set_defaults(func=py_edit)
|
1294
|
+
|
1295
|
+
# Show help when running without commands
|
1296
|
+
if len(sys.argv) == 1:
|
1297
|
+
parser.print_help(sys.stderr)
|
1298
|
+
sys.exit(1)
|
1299
|
+
|
1300
|
+
# Boilerplate
|
1301
|
+
args, _ = parser.parse_known_args()
|
1302
|
+
args.func(args)
|
1303
|
+
|
1304
|
+
|
1305
|
+
if __name__ == "__main__":
|
1306
|
+
main()
|