docassemblecli3 0.2.2__tar.gz → 0.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/PKG-INFO +36 -7
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/README.md +34 -5
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/docassemblecli3/docassemblecli3.py +395 -102
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/pyproject.toml +3 -3
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/LICENSE +0 -0
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/docassemblecli3/__init__.py +0 -0
- {docassemblecli3-0.2.2 → docassemblecli3-0.3.6}/docassemblecli3/__main__.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: docassemblecli3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Multi-platform CLI utility for working with docassemble packages and servers
|
|
5
5
|
Keywords: docassemble
|
|
6
6
|
Author-email: Jack Adamson <jackadamson@gmail.com>, Jonathan Pyle <jhpyle@gmail.com>
|
|
7
|
-
Requires-Python: >= 3.
|
|
7
|
+
Requires-Python: >= 3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
@@ -41,7 +41,7 @@ released under the MIT License.
|
|
|
41
41
|
|
|
42
42
|
## Prerequisites
|
|
43
43
|
|
|
44
|
-
This program should only require that you have Python 3.
|
|
44
|
+
This program should only require that you have Python 3.10 installed on your
|
|
45
45
|
computer, but it was developed and tested with Python 3.12. Please report any
|
|
46
46
|
bugs or errors you experience.
|
|
47
47
|
|
|
@@ -172,7 +172,7 @@ works:
|
|
|
172
172
|
directory]
|
|
173
173
|
-c, --config PATH Specify the config file to use or leave it
|
|
174
174
|
blank to skip using any config file [default:
|
|
175
|
-
C:\Users\
|
|
175
|
+
C:\Users\current_user\.docassemblecli]
|
|
176
176
|
-p, --playground (PROJECT) Install into the default Playground or into the
|
|
177
177
|
specified Playground project.
|
|
178
178
|
-r, --restart [yes|no|auto] On package install: yes, force a restart | no,
|
|
@@ -262,7 +262,7 @@ works:
|
|
|
262
262
|
directory]
|
|
263
263
|
-c, --config PATH Specify the config file to use or leave it
|
|
264
264
|
blank to skip using any config file [default:
|
|
265
|
-
C:\Users\
|
|
265
|
+
C:\Users\current_user\.docassemblecli]
|
|
266
266
|
-p, --playground (PROJECT) Install into the default Playground or into the
|
|
267
267
|
specified Playground project.
|
|
268
268
|
-a, --api <URL TEXT>... URL of the docassemble server and API key of
|
|
@@ -282,11 +282,18 @@ Your package's `.gitignore` file is also used by `watch` to decide which files
|
|
|
282
282
|
to ignore. If you don't have a `.gitignore` file in your package, then the
|
|
283
283
|
default `.gitignore` that `create` makes is used instead. The `.git/` directory
|
|
284
284
|
and `.gitignore` file are both also ignored by `watch` (note: don't add them to
|
|
285
|
-
your `.gitignore`).
|
|
285
|
+
your `.gitignore`). The following directories are always ignored by `watch`:
|
|
286
|
+
`.git`, `__pycache__`, `.mypy_cache`, `.venv`, `.history`, `build`.
|
|
287
|
+
|
|
288
|
+
If you have a `directory` key for the server in your `.docassemblecli` config
|
|
289
|
+
file, it will cause that server to be used if no `server` is provided and the
|
|
290
|
+
`directory` matches the `directory` that `watch` was given
|
|
291
|
+
[default: current directory]. Additionally, if there is a `playground` key for
|
|
292
|
+
that server, it will be used when using `watch`.
|
|
286
293
|
|
|
287
294
|
#### watchdog
|
|
288
295
|
|
|
289
|
-
The `watch` command
|
|
296
|
+
The `watch` command depends on the
|
|
290
297
|
[watchdog](https://pypi.org/project/watchdog/) Python package. This allows
|
|
291
298
|
`watch` to work on the following platforms that [watchdog] supports:
|
|
292
299
|
|
|
@@ -336,6 +343,28 @@ easy to use and will prompt you as necessary.
|
|
|
336
343
|
remove Remove a server from the config file.
|
|
337
344
|
test Test the URL and API key.
|
|
338
345
|
|
|
346
|
+
The `.docassemblecli` config file can store additional configuration for each server:
|
|
347
|
+
|
|
348
|
+
- `directory`: If the current or provided directory matches this then this server will be selected
|
|
349
|
+
- `playground`: Default playground project to use if `directory` matches
|
|
350
|
+
- `startup`: If set to "install", automatically installs package when `watch` starts
|
|
351
|
+
|
|
352
|
+
For example, your `.docassemblecli` file might look like this:
|
|
353
|
+
|
|
354
|
+
```yaml
|
|
355
|
+
- apiurl: https://da.example.com
|
|
356
|
+
apikey: H3PWMKJOIVAXL4PWUJH3HG7EKPFU5GYT
|
|
357
|
+
name: da.example.com
|
|
358
|
+
playground: testing
|
|
359
|
+
directory: /path/to/docassemble-mypackage
|
|
360
|
+
startup: install
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
With this configuration:
|
|
364
|
+
- `da watch` in `/path/to/docassemble-mypackage` will automatically use this server
|
|
365
|
+
- It will install to the "testing" playground project
|
|
366
|
+
- It will install the package once when `watch` starts
|
|
367
|
+
|
|
339
368
|
## How it works
|
|
340
369
|
|
|
341
370
|
The `install` command is just a simple Python script that creates a ZIP file and
|
|
@@ -22,7 +22,7 @@ released under the MIT License.
|
|
|
22
22
|
|
|
23
23
|
## Prerequisites
|
|
24
24
|
|
|
25
|
-
This program should only require that you have Python 3.
|
|
25
|
+
This program should only require that you have Python 3.10 installed on your
|
|
26
26
|
computer, but it was developed and tested with Python 3.12. Please report any
|
|
27
27
|
bugs or errors you experience.
|
|
28
28
|
|
|
@@ -153,7 +153,7 @@ works:
|
|
|
153
153
|
directory]
|
|
154
154
|
-c, --config PATH Specify the config file to use or leave it
|
|
155
155
|
blank to skip using any config file [default:
|
|
156
|
-
C:\Users\
|
|
156
|
+
C:\Users\current_user\.docassemblecli]
|
|
157
157
|
-p, --playground (PROJECT) Install into the default Playground or into the
|
|
158
158
|
specified Playground project.
|
|
159
159
|
-r, --restart [yes|no|auto] On package install: yes, force a restart | no,
|
|
@@ -243,7 +243,7 @@ works:
|
|
|
243
243
|
directory]
|
|
244
244
|
-c, --config PATH Specify the config file to use or leave it
|
|
245
245
|
blank to skip using any config file [default:
|
|
246
|
-
C:\Users\
|
|
246
|
+
C:\Users\current_user\.docassemblecli]
|
|
247
247
|
-p, --playground (PROJECT) Install into the default Playground or into the
|
|
248
248
|
specified Playground project.
|
|
249
249
|
-a, --api <URL TEXT>... URL of the docassemble server and API key of
|
|
@@ -263,11 +263,18 @@ Your package's `.gitignore` file is also used by `watch` to decide which files
|
|
|
263
263
|
to ignore. If you don't have a `.gitignore` file in your package, then the
|
|
264
264
|
default `.gitignore` that `create` makes is used instead. The `.git/` directory
|
|
265
265
|
and `.gitignore` file are both also ignored by `watch` (note: don't add them to
|
|
266
|
-
your `.gitignore`).
|
|
266
|
+
your `.gitignore`). The following directories are always ignored by `watch`:
|
|
267
|
+
`.git`, `__pycache__`, `.mypy_cache`, `.venv`, `.history`, `build`.
|
|
268
|
+
|
|
269
|
+
If you have a `directory` key for the server in your `.docassemblecli` config
|
|
270
|
+
file, it will cause that server to be used if no `server` is provided and the
|
|
271
|
+
`directory` matches the `directory` that `watch` was given
|
|
272
|
+
[default: current directory]. Additionally, if there is a `playground` key for
|
|
273
|
+
that server, it will be used when using `watch`.
|
|
267
274
|
|
|
268
275
|
#### watchdog
|
|
269
276
|
|
|
270
|
-
The `watch` command
|
|
277
|
+
The `watch` command depends on the
|
|
271
278
|
[watchdog](https://pypi.org/project/watchdog/) Python package. This allows
|
|
272
279
|
`watch` to work on the following platforms that [watchdog] supports:
|
|
273
280
|
|
|
@@ -317,6 +324,28 @@ easy to use and will prompt you as necessary.
|
|
|
317
324
|
remove Remove a server from the config file.
|
|
318
325
|
test Test the URL and API key.
|
|
319
326
|
|
|
327
|
+
The `.docassemblecli` config file can store additional configuration for each server:
|
|
328
|
+
|
|
329
|
+
- `directory`: If the current or provided directory matches this then this server will be selected
|
|
330
|
+
- `playground`: Default playground project to use if `directory` matches
|
|
331
|
+
- `startup`: If set to "install", automatically installs package when `watch` starts
|
|
332
|
+
|
|
333
|
+
For example, your `.docassemblecli` file might look like this:
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
- apiurl: https://da.example.com
|
|
337
|
+
apikey: H3PWMKJOIVAXL4PWUJH3HG7EKPFU5GYT
|
|
338
|
+
name: da.example.com
|
|
339
|
+
playground: testing
|
|
340
|
+
directory: /path/to/docassemble-mypackage
|
|
341
|
+
startup: install
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
With this configuration:
|
|
345
|
+
- `da watch` in `/path/to/docassemble-mypackage` will automatically use this server
|
|
346
|
+
- It will install to the "testing" playground project
|
|
347
|
+
- It will install the package once when `watch` starts
|
|
348
|
+
|
|
320
349
|
## How it works
|
|
321
350
|
|
|
322
351
|
The `install` command is just a simple Python script that creates a ZIP file and
|
|
@@ -18,6 +18,7 @@ from packaging import version as packaging_version
|
|
|
18
18
|
from watchdog.events import FileSystemEventHandler
|
|
19
19
|
from watchdog.observers import Observer
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
global DEFAULT_CONFIG
|
|
22
23
|
DEFAULT_CONFIG = os.path.join(os.path.expanduser("~"), ".docassemblecli")
|
|
23
24
|
|
|
@@ -34,6 +35,12 @@ FILE_CHECKSUMS = {}
|
|
|
34
35
|
global DEBUG
|
|
35
36
|
DEBUG = False
|
|
36
37
|
|
|
38
|
+
global EXCLUDED_DIRECTORIES
|
|
39
|
+
EXCLUDED_DIRECTORIES = [".git", "__pycache__", ".mypy_cache", ".venv", ".history", "build"]
|
|
40
|
+
|
|
41
|
+
global GITMATCH_COMPILED
|
|
42
|
+
GITMATCH_COMPILED = None
|
|
43
|
+
|
|
37
44
|
global GITIGNORE
|
|
38
45
|
GITIGNORE = """\
|
|
39
46
|
__pycache__/
|
|
@@ -87,12 +94,19 @@ share/python-wheels/
|
|
|
87
94
|
# click
|
|
88
95
|
# -----------------------------------------------------------------------------
|
|
89
96
|
|
|
97
|
+
|
|
90
98
|
CONTEXT_SETTINGS = dict(help_option_names=["--help", "-h"])
|
|
91
99
|
|
|
92
100
|
|
|
93
101
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
94
102
|
@click.version_option()
|
|
95
|
-
@click.option(
|
|
103
|
+
@click.option(
|
|
104
|
+
"--color/--no-color",
|
|
105
|
+
"-C/-N",
|
|
106
|
+
default=None,
|
|
107
|
+
show_default=True,
|
|
108
|
+
help="Overrides color auto-detection in interactive terminals.",
|
|
109
|
+
)
|
|
96
110
|
@click.option("--debug/--no-debug", default=False, hidden=True)
|
|
97
111
|
def cli(color, debug):
|
|
98
112
|
"""
|
|
@@ -113,34 +127,76 @@ def config():
|
|
|
113
127
|
|
|
114
128
|
|
|
115
129
|
def common_params_for_api(func):
|
|
116
|
-
@click.option(
|
|
130
|
+
@click.option(
|
|
131
|
+
"--api",
|
|
132
|
+
"-a",
|
|
133
|
+
type=(APIURLType(), str),
|
|
134
|
+
default=(None, None),
|
|
135
|
+
help="URL of the docassemble server and API key of the user (admin or developer)",
|
|
136
|
+
)
|
|
117
137
|
@click.option("--server", "-s", metavar="SERVER", default="", help="Specify a server from the config file")
|
|
118
138
|
@wraps(func)
|
|
119
139
|
def wrapper(*args, **kwargs):
|
|
120
140
|
return func(*args, **kwargs)
|
|
141
|
+
|
|
121
142
|
return wrapper
|
|
122
143
|
|
|
123
144
|
|
|
124
145
|
def common_params_for_config(func):
|
|
125
|
-
@click.option(
|
|
146
|
+
@click.option(
|
|
147
|
+
"--config",
|
|
148
|
+
"-c",
|
|
149
|
+
default=DEFAULT_CONFIG,
|
|
150
|
+
type=click.Path(),
|
|
151
|
+
callback=validate_and_load_or_create_config,
|
|
152
|
+
show_default=True,
|
|
153
|
+
help="Specify the config file to use",
|
|
154
|
+
)
|
|
126
155
|
@wraps(func)
|
|
127
156
|
def wrapper(*args, **kwargs):
|
|
128
157
|
return func(*args, **kwargs)
|
|
158
|
+
|
|
129
159
|
return wrapper
|
|
130
160
|
|
|
131
161
|
|
|
132
162
|
def common_params_for_installation(func):
|
|
133
|
-
@click.option(
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
@click.option(
|
|
164
|
+
"--directory",
|
|
165
|
+
"-d",
|
|
166
|
+
default=os.getcwd(),
|
|
167
|
+
type=click.Path(),
|
|
168
|
+
callback=validate_package_directory,
|
|
169
|
+
help="Specify package directory [default: current directory]",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--config",
|
|
173
|
+
"-c",
|
|
174
|
+
is_flag=False,
|
|
175
|
+
flag_value="",
|
|
176
|
+
default=DEFAULT_CONFIG,
|
|
177
|
+
type=click.Path(),
|
|
178
|
+
callback=validate_and_load_or_create_config,
|
|
179
|
+
show_default=True,
|
|
180
|
+
help="Specify the config file to use or leave it blank to skip using any config file",
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
"--playground",
|
|
184
|
+
"-p",
|
|
185
|
+
metavar="(PROJECT)",
|
|
186
|
+
is_flag=False,
|
|
187
|
+
flag_value="default",
|
|
188
|
+
help="Install into the default Playground or into the specified Playground project.",
|
|
189
|
+
)
|
|
136
190
|
@wraps(func)
|
|
137
191
|
def wrapper(*args, **kwargs):
|
|
138
192
|
return func(*args, **kwargs)
|
|
193
|
+
|
|
139
194
|
return wrapper
|
|
140
195
|
|
|
141
196
|
|
|
142
197
|
class APIURLType(click.ParamType):
|
|
143
198
|
name = "url"
|
|
199
|
+
|
|
144
200
|
def convert(self, value, param, ctx):
|
|
145
201
|
parsed_url = urlparse(value)
|
|
146
202
|
if all([re.search(r"""^https?://[^\s]+$""", value), parsed_url.scheme, parsed_url.netloc]):
|
|
@@ -154,7 +210,9 @@ def validate_package_directory(ctx, param, directory: str) -> str:
|
|
|
154
210
|
if not os.path.exists(directory):
|
|
155
211
|
raise click.BadParameter(f"""Directory "{directory}" does not exist.""")
|
|
156
212
|
if not os.path.isfile(os.path.join(directory, "setup.py")):
|
|
157
|
-
raise click.BadParameter(
|
|
213
|
+
raise click.BadParameter(
|
|
214
|
+
f"""Directory "{directory}" does not contain a setup.py file, so it is not the directory of a valid Python package."""
|
|
215
|
+
)
|
|
158
216
|
else:
|
|
159
217
|
return directory
|
|
160
218
|
|
|
@@ -185,6 +243,7 @@ def validate_and_load_or_create_config(ctx, param, config: str) -> tuple[str, li
|
|
|
185
243
|
# utility functions
|
|
186
244
|
# -----------------------------------------------------------------------------
|
|
187
245
|
|
|
246
|
+
|
|
188
247
|
def name_from_url(url: str) -> str:
|
|
189
248
|
if not url:
|
|
190
249
|
return ""
|
|
@@ -200,10 +259,18 @@ def display_servers(env: list = None) -> list[str]:
|
|
|
200
259
|
servers.append(item.get("name", ""))
|
|
201
260
|
else:
|
|
202
261
|
servers.append(item.get("name", "") + " (default)")
|
|
262
|
+
if "playground" in item:
|
|
263
|
+
servers.append(f""" playground: {item["playground"]}""")
|
|
264
|
+
if "directory" in item:
|
|
265
|
+
servers.append(f""" directory: {item["directory"]}""")
|
|
266
|
+
if "startup" in item:
|
|
267
|
+
servers.append(f""" startup: {item["startup"]}""")
|
|
203
268
|
return servers
|
|
204
269
|
|
|
205
270
|
|
|
206
|
-
def select_server(
|
|
271
|
+
def select_server(
|
|
272
|
+
cfg: str = None, env: list = None, apiurl: str = None, apikey: str = None, server: str = "", **kwargs
|
|
273
|
+
) -> dict:
|
|
207
274
|
if apiurl and apikey:
|
|
208
275
|
return add_server_to_env(cfg=cfg, env=env, apiurl=apiurl, apikey=apikey)[-1]
|
|
209
276
|
if isinstance(env, list):
|
|
@@ -216,6 +283,10 @@ def select_server(cfg: str = None, env: list = None, apiurl: str = None, apikey:
|
|
|
216
283
|
return item
|
|
217
284
|
raise click.BadParameter(f"""Server "{server}" was not found.""", param_hint="--server")
|
|
218
285
|
if len(env) > 0:
|
|
286
|
+
if "directory" in kwargs:
|
|
287
|
+
for item in env:
|
|
288
|
+
if item.get("directory", None) == kwargs["directory"]:
|
|
289
|
+
return item
|
|
219
290
|
return env[0]
|
|
220
291
|
if "DOCASSEMBLEAPIURL" in os.environ and "DOCASSEMBLEAPIKEY" in os.environ:
|
|
221
292
|
apiurl: str = os.environ["DOCASSEMBLEAPIURL"]
|
|
@@ -224,7 +295,9 @@ def select_server(cfg: str = None, env: list = None, apiurl: str = None, apikey:
|
|
|
224
295
|
return add_server_to_env(cfg, env)[0]
|
|
225
296
|
|
|
226
297
|
|
|
227
|
-
def add_or_update_env(
|
|
298
|
+
def add_or_update_env(
|
|
299
|
+
env: list = None, apiurl: str = "", apikey: str = "", directory: str = "", playground: str = ""
|
|
300
|
+
) -> list:
|
|
228
301
|
if not env:
|
|
229
302
|
env: list = []
|
|
230
303
|
apiname: str = name_from_url(apiurl)
|
|
@@ -233,11 +306,20 @@ def add_or_update_env(env: list = None, apiurl: str = "", apikey: str = "") -> l
|
|
|
233
306
|
if item.get("name", None) == apiname:
|
|
234
307
|
item["apiurl"] = apiurl
|
|
235
308
|
item["apikey"] = apikey
|
|
309
|
+
if directory:
|
|
310
|
+
item["directory"] = directory
|
|
311
|
+
if playground:
|
|
312
|
+
item["playground"] = playground
|
|
236
313
|
found = True
|
|
237
314
|
click.echo(f"""Server "{apiname}" was found and updated.""")
|
|
238
315
|
break
|
|
239
316
|
if not found:
|
|
240
|
-
|
|
317
|
+
new_server = {"apiurl": apiurl, "apikey": apikey, "name": apiname}
|
|
318
|
+
if directory:
|
|
319
|
+
new_server["directory"] = directory
|
|
320
|
+
if playground:
|
|
321
|
+
new_server["playground"] = playground
|
|
322
|
+
env.append(new_server)
|
|
241
323
|
return env
|
|
242
324
|
|
|
243
325
|
|
|
@@ -256,7 +338,11 @@ def prompt_for_api(retry: str = False, previous_url: str = None, previous_key: s
|
|
|
256
338
|
if retry:
|
|
257
339
|
if not click.confirm("Do you want to try another URL and API key?", default=True):
|
|
258
340
|
raise click.Abort()
|
|
259
|
-
apiurl = click.prompt(
|
|
341
|
+
apiurl = click.prompt(
|
|
342
|
+
"""Base URL of your docassemble server (e.g., https://da.example.com)""",
|
|
343
|
+
type=APIURLType(),
|
|
344
|
+
default=previous_url,
|
|
345
|
+
)
|
|
260
346
|
apikey = click.prompt(f"""API key of admin or developer user on {apiurl}""", default=previous_key).strip()
|
|
261
347
|
return apiurl, apikey
|
|
262
348
|
|
|
@@ -267,9 +353,13 @@ def test_apiurl_apikey(apiurl: str, apikey: str) -> bool:
|
|
|
267
353
|
api_test = requests.get(apiurl + "/api/package", headers={"X-API-Key": apikey})
|
|
268
354
|
if api_test.status_code != 200:
|
|
269
355
|
if api_test.status_code == 403:
|
|
270
|
-
click.secho(
|
|
356
|
+
click.secho(
|
|
357
|
+
f"""\nThe API KEY is invalid. ({api_test.status_code} {api_test.text.strip()})\n""", fg="red"
|
|
358
|
+
)
|
|
271
359
|
else:
|
|
272
|
-
click.secho(
|
|
360
|
+
click.secho(
|
|
361
|
+
f"""\nThe API URL or KEY is invalid. ({api_test.status_code} {api_test.text.strip()})\n""", fg="red"
|
|
362
|
+
)
|
|
273
363
|
return False
|
|
274
364
|
except Exception as err:
|
|
275
365
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
@@ -279,26 +369,26 @@ def test_apiurl_apikey(apiurl: str, apikey: str) -> bool:
|
|
|
279
369
|
return True
|
|
280
370
|
|
|
281
371
|
|
|
282
|
-
def add_server_to_env(
|
|
372
|
+
def add_server_to_env(
|
|
373
|
+
cfg: str = None,
|
|
374
|
+
env: list = None,
|
|
375
|
+
apiurl: str = None,
|
|
376
|
+
apikey: str = None,
|
|
377
|
+
directory: str = None,
|
|
378
|
+
playground: str = None,
|
|
379
|
+
):
|
|
283
380
|
if not apiurl or not apikey:
|
|
284
381
|
apiurl, apikey = prompt_for_api()
|
|
285
382
|
while not test_apiurl_apikey(apiurl=apiurl, apikey=apikey):
|
|
286
383
|
apiurl, apikey = prompt_for_api(retry=True, previous_url=apiurl, previous_key=apikey)
|
|
287
|
-
env = add_or_update_env(env=env, apiurl=apiurl, apikey=apikey)
|
|
384
|
+
env = add_or_update_env(env=env, apiurl=apiurl, apikey=apikey, directory=directory, playground=playground)
|
|
288
385
|
if cfg:
|
|
289
386
|
if save_config(cfg, env):
|
|
290
387
|
click.echo(f"""Configuration saved: {cfg}""")
|
|
291
388
|
return env
|
|
292
389
|
|
|
293
390
|
|
|
294
|
-
def
|
|
295
|
-
if apiurl and apikey:
|
|
296
|
-
return add_server_to_env(cfg=cfg, env=env, apiurl=apiurl, apikey=apikey)[-1]
|
|
297
|
-
else:
|
|
298
|
-
return select_server(cfg=cfg, env=env, server=server)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, server_version_da: str = "0"):
|
|
391
|
+
def wait_for_server(playground: bool, task_id: str, apikey: str, apiurl: str, server_version_da: str = "0"):
|
|
302
392
|
click.secho("Waiting for package to install...", fg="cyan")
|
|
303
393
|
tries = 0
|
|
304
394
|
before_wait_for_server = time.time()
|
|
@@ -312,7 +402,7 @@ def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, ser
|
|
|
312
402
|
except requests.exceptions.RequestException:
|
|
313
403
|
pass
|
|
314
404
|
if r.status_code != 200:
|
|
315
|
-
return
|
|
405
|
+
return "package_update_status returned " + str(r.status_code) + ": " + r.text
|
|
316
406
|
info = r.json()
|
|
317
407
|
if info["status"] == "completed" or info["status"] == "unknown":
|
|
318
408
|
break
|
|
@@ -325,7 +415,10 @@ def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, ser
|
|
|
325
415
|
success = True
|
|
326
416
|
elif info.get("ok", False):
|
|
327
417
|
success = True
|
|
328
|
-
if not (
|
|
418
|
+
if not (
|
|
419
|
+
server_version_da == "norestart"
|
|
420
|
+
or packaging_version.parse(server_version_da) >= packaging_version.parse("1.5.3")
|
|
421
|
+
):
|
|
329
422
|
if DEBUG:
|
|
330
423
|
click.echo(f"""Package install duration: {(after_wait_for_server - before_wait_for_server):.2f}s""")
|
|
331
424
|
click.echo("""Manually waiting for background processes.""")
|
|
@@ -345,11 +438,19 @@ def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, ser
|
|
|
345
438
|
# package_installer
|
|
346
439
|
# -----------------------------------------------------------------------------
|
|
347
440
|
|
|
441
|
+
|
|
348
442
|
def package_installer(directory, apiurl, apikey, playground, restart):
|
|
349
443
|
archive = tempfile.NamedTemporaryFile(suffix=".zip")
|
|
350
444
|
zf = zipfile.ZipFile(archive, compression=zipfile.ZIP_DEFLATED, mode="w")
|
|
351
445
|
try:
|
|
352
|
-
ignore_process = subprocess.run(
|
|
446
|
+
ignore_process = subprocess.run(
|
|
447
|
+
["git", "ls-files", "-i", "--directory", "-o", "--exclude-standard"],
|
|
448
|
+
stdout=subprocess.PIPE,
|
|
449
|
+
stderr=subprocess.PIPE,
|
|
450
|
+
universal_newlines=True,
|
|
451
|
+
cwd=directory,
|
|
452
|
+
check=False,
|
|
453
|
+
)
|
|
353
454
|
ignore_process.check_returncode()
|
|
354
455
|
raw_ignore = ignore_process.stdout.splitlines()
|
|
355
456
|
except Exception:
|
|
@@ -361,7 +462,13 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
361
462
|
dependencies = {}
|
|
362
463
|
for root, dirs, files in os.walk(directory, topdown=True):
|
|
363
464
|
adjusted_root = os.sep.join(root.split(os.sep)[1:])
|
|
364
|
-
dirs[:] = [
|
|
465
|
+
dirs[:] = [
|
|
466
|
+
d
|
|
467
|
+
for d in dirs
|
|
468
|
+
if d not in EXCLUDED_DIRECTORIES
|
|
469
|
+
and not d.endswith(".egg-info")
|
|
470
|
+
and os.path.join(adjusted_root, d) not in to_ignore
|
|
471
|
+
]
|
|
365
472
|
if root_directory is None and ("setup.py" in files or "setup.cfg" in files):
|
|
366
473
|
root_directory = root
|
|
367
474
|
if "setup.py" in files:
|
|
@@ -374,24 +481,48 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
374
481
|
if m:
|
|
375
482
|
for package_text in m.group(1).split(","):
|
|
376
483
|
package_name = package_text.strip()
|
|
377
|
-
if
|
|
484
|
+
if (
|
|
485
|
+
len(package_name) >= 3
|
|
486
|
+
and package_name[0] == package_name[-1]
|
|
487
|
+
and package_name[0] in (""", """)
|
|
488
|
+
):
|
|
378
489
|
package_name = package_name[1:-1]
|
|
379
490
|
mm = re.search(r"""(.*)(<=|>=|==|<|>)(.*)""", package_name)
|
|
380
491
|
if mm:
|
|
381
|
-
dependencies[mm.group(1).strip()] = {
|
|
492
|
+
dependencies[mm.group(1).strip()] = {
|
|
493
|
+
"installed": False,
|
|
494
|
+
"operator": mm.group(2),
|
|
495
|
+
"version": mm.group(3).strip(),
|
|
496
|
+
}
|
|
382
497
|
else:
|
|
383
498
|
dependencies[package_name] = {"installed": False, "operator": None, "version": None}
|
|
384
499
|
for the_file in files:
|
|
385
|
-
if
|
|
500
|
+
if (
|
|
501
|
+
the_file.endswith("~")
|
|
502
|
+
or the_file.endswith(".pyc")
|
|
503
|
+
or the_file.endswith(".swp")
|
|
504
|
+
or the_file.startswith("#")
|
|
505
|
+
or the_file.startswith(".#")
|
|
506
|
+
or (the_file == ".gitignore" and root_directory == root)
|
|
507
|
+
or os.path.join(adjusted_root, the_file) in to_ignore
|
|
508
|
+
):
|
|
386
509
|
continue
|
|
387
|
-
if
|
|
510
|
+
if (
|
|
511
|
+
not has_python_files
|
|
512
|
+
and the_file.endswith(".py")
|
|
513
|
+
and not (the_file == "setup.py" and root == root_directory)
|
|
514
|
+
and the_file != "__init__.py"
|
|
515
|
+
):
|
|
388
516
|
has_python_files = True
|
|
389
|
-
zf.write(
|
|
517
|
+
zf.write(
|
|
518
|
+
os.path.join(root, the_file),
|
|
519
|
+
os.path.relpath(os.path.join(root, the_file), os.path.join(directory, "..")),
|
|
520
|
+
)
|
|
390
521
|
zf.close()
|
|
391
522
|
archive.seek(0)
|
|
392
523
|
if restart == "no":
|
|
393
524
|
should_restart = False
|
|
394
|
-
elif restart =="yes" or has_python_files:
|
|
525
|
+
elif restart == "yes" or has_python_files:
|
|
395
526
|
should_restart = True
|
|
396
527
|
elif len(dependencies) > 0 or this_package_name:
|
|
397
528
|
try:
|
|
@@ -400,7 +531,7 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
400
531
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
401
532
|
raise click.ClickException(f"""{err}\n""")
|
|
402
533
|
if r.status_code != 200:
|
|
403
|
-
return
|
|
534
|
+
return "/api/package returned " + str(r.status_code) + ": " + r.text
|
|
404
535
|
installed_packages = r.json()
|
|
405
536
|
already_installed = False
|
|
406
537
|
for package_info in installed_packages:
|
|
@@ -410,20 +541,33 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
410
541
|
condition = True
|
|
411
542
|
if dependency_info["operator"]:
|
|
412
543
|
if dependency_info["operator"] == "==":
|
|
413
|
-
condition = packaging_version.parse(package_info["version"]) == packaging_version.parse(
|
|
544
|
+
condition = packaging_version.parse(package_info["version"]) == packaging_version.parse(
|
|
545
|
+
dependency_info["version"]
|
|
546
|
+
)
|
|
414
547
|
elif dependency_info["operator"] == "<=":
|
|
415
|
-
condition = packaging_version.parse(package_info["version"]) <= packaging_version.parse(
|
|
548
|
+
condition = packaging_version.parse(package_info["version"]) <= packaging_version.parse(
|
|
549
|
+
dependency_info["version"]
|
|
550
|
+
)
|
|
416
551
|
elif dependency_info["operator"] == ">=":
|
|
417
|
-
condition = packaging_version.parse(package_info["version"]) >= packaging_version.parse(
|
|
552
|
+
condition = packaging_version.parse(package_info["version"]) >= packaging_version.parse(
|
|
553
|
+
dependency_info["version"]
|
|
554
|
+
)
|
|
418
555
|
elif dependency_info["operator"] == "<":
|
|
419
|
-
condition = packaging_version.parse(package_info["version"]) < packaging_version.parse(
|
|
556
|
+
condition = packaging_version.parse(package_info["version"]) < packaging_version.parse(
|
|
557
|
+
dependency_info["version"]
|
|
558
|
+
)
|
|
420
559
|
elif dependency_info["operator"] == ">":
|
|
421
|
-
condition = packaging_version.parse(package_info["version"]) > packaging_version.parse(
|
|
560
|
+
condition = packaging_version.parse(package_info["version"]) > packaging_version.parse(
|
|
561
|
+
dependency_info["version"]
|
|
562
|
+
)
|
|
422
563
|
if condition:
|
|
423
564
|
dependency_info["installed"] = True
|
|
424
565
|
if this_package_name and this_package_name in (package_info["name"], package_info["alt_name"]):
|
|
425
566
|
already_installed = True
|
|
426
|
-
should_restart = bool(
|
|
567
|
+
should_restart = bool(
|
|
568
|
+
(not already_installed and len(dependencies) > 0)
|
|
569
|
+
or not all(item["installed"] for item in dependencies.values())
|
|
570
|
+
)
|
|
427
571
|
else:
|
|
428
572
|
should_restart = True
|
|
429
573
|
data = {}
|
|
@@ -458,12 +602,20 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
458
602
|
try:
|
|
459
603
|
requests.post(project_endpoint, data={"project": playground}, headers={"X-API-Key": apikey})
|
|
460
604
|
except Exception:
|
|
461
|
-
return
|
|
605
|
+
return "create project POST returned " + project_list.text
|
|
462
606
|
else:
|
|
463
607
|
click.echo("\n")
|
|
464
|
-
return
|
|
608
|
+
return (
|
|
609
|
+
"playground list of projects GET returned " + str(project_list.status_code) + ": " + project_list.text
|
|
610
|
+
)
|
|
465
611
|
try:
|
|
466
|
-
r = requests.post(
|
|
612
|
+
r = requests.post(
|
|
613
|
+
apiurl + "/api/playground_install",
|
|
614
|
+
data=data,
|
|
615
|
+
files={"file": archive},
|
|
616
|
+
headers={"X-API-Key": apikey},
|
|
617
|
+
timeout=600,
|
|
618
|
+
)
|
|
467
619
|
except Exception as err:
|
|
468
620
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
469
621
|
raise click.ClickException(f"""{err}\n""")
|
|
@@ -473,17 +625,33 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
473
625
|
except Exception:
|
|
474
626
|
error_message = ""
|
|
475
627
|
if "project" not in data or error_message != "Invalid project.":
|
|
476
|
-
return
|
|
628
|
+
return "playground_install POST returned " + str(r.status_code) + ": " + r.text
|
|
477
629
|
try:
|
|
478
|
-
r = requests.post(
|
|
630
|
+
r = requests.post(
|
|
631
|
+
apiurl + "/api/playground/project",
|
|
632
|
+
data={"project": data["project"]},
|
|
633
|
+
headers={"X-API-Key": apikey},
|
|
634
|
+
timeout=600,
|
|
635
|
+
)
|
|
479
636
|
except Exception as err:
|
|
480
637
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
481
638
|
raise click.ClickException(f"""{err}\n""")
|
|
482
639
|
if r.status_code != 204:
|
|
483
|
-
return
|
|
640
|
+
return (
|
|
641
|
+
"needed to create playground project but POST to api/playground/project returned "
|
|
642
|
+
+ str(r.status_code)
|
|
643
|
+
+ ": "
|
|
644
|
+
+ r.text
|
|
645
|
+
)
|
|
484
646
|
archive.seek(0)
|
|
485
647
|
try:
|
|
486
|
-
r = requests.post(
|
|
648
|
+
r = requests.post(
|
|
649
|
+
apiurl + "/api/playground_install",
|
|
650
|
+
data=data,
|
|
651
|
+
files={"file": archive},
|
|
652
|
+
headers={"X-API-Key": apikey},
|
|
653
|
+
timeout=600,
|
|
654
|
+
)
|
|
487
655
|
except Exception as err:
|
|
488
656
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
489
657
|
raise click.ClickException(f"""{err}\n""")
|
|
@@ -491,14 +659,20 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
491
659
|
try:
|
|
492
660
|
info = r.json()
|
|
493
661
|
except Exception:
|
|
494
|
-
return
|
|
662
|
+
return r.text
|
|
495
663
|
task_id = info["task_id"]
|
|
496
|
-
success = wait_for_server(
|
|
664
|
+
success = wait_for_server(
|
|
665
|
+
playground=bool(playground),
|
|
666
|
+
task_id=task_id,
|
|
667
|
+
apikey=apikey,
|
|
668
|
+
apiurl=apiurl,
|
|
669
|
+
server_version_da=server_version_da,
|
|
670
|
+
)
|
|
497
671
|
elif r.status_code == 204:
|
|
498
672
|
success = True
|
|
499
673
|
else:
|
|
500
674
|
click.echo("\n")
|
|
501
|
-
return
|
|
675
|
+
return "playground_install POST returned " + str(r.status_code) + ": " + r.text
|
|
502
676
|
if success:
|
|
503
677
|
click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Installed.""", fg="green")
|
|
504
678
|
else:
|
|
@@ -506,15 +680,23 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
506
680
|
return 1
|
|
507
681
|
else:
|
|
508
682
|
try:
|
|
509
|
-
r = requests.post(
|
|
683
|
+
r = requests.post(
|
|
684
|
+
apiurl + "/api/package", data=data, files={"zip": archive}, headers={"X-API-Key": apikey}, timeout=600
|
|
685
|
+
)
|
|
510
686
|
except Exception as err:
|
|
511
687
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
512
688
|
raise click.ClickException(f"""{err}\n""")
|
|
513
689
|
if r.status_code != 200:
|
|
514
|
-
return
|
|
690
|
+
return "package POST returned " + str(r.status_code) + ": " + r.text
|
|
515
691
|
info = r.json()
|
|
516
692
|
task_id = info["task_id"]
|
|
517
|
-
if wait_for_server(
|
|
693
|
+
if wait_for_server(
|
|
694
|
+
playground=bool(playground),
|
|
695
|
+
task_id=task_id,
|
|
696
|
+
apikey=apikey,
|
|
697
|
+
apiurl=apiurl,
|
|
698
|
+
server_version_da=server_version_da,
|
|
699
|
+
):
|
|
518
700
|
click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Installed.""", fg="green")
|
|
519
701
|
if not should_restart:
|
|
520
702
|
try:
|
|
@@ -523,7 +705,7 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
523
705
|
click.secho(f"""\n{err.__class__.__name__}""", fg="red")
|
|
524
706
|
raise click.ClickException(f"""{err}\n""")
|
|
525
707
|
if r.status_code != 204:
|
|
526
|
-
return
|
|
708
|
+
return "clear_cache returned " + str(r.status_code) + ": " + r.text
|
|
527
709
|
return 0
|
|
528
710
|
|
|
529
711
|
|
|
@@ -531,10 +713,18 @@ def package_installer(directory, apiurl, apikey, playground, restart):
|
|
|
531
713
|
# install
|
|
532
714
|
# =============================================================================
|
|
533
715
|
|
|
716
|
+
|
|
534
717
|
@cli.command(context_settings=CONTEXT_SETTINGS)
|
|
535
718
|
@common_params_for_api
|
|
536
719
|
@common_params_for_installation
|
|
537
|
-
@click.option(
|
|
720
|
+
@click.option(
|
|
721
|
+
"--restart",
|
|
722
|
+
"-r",
|
|
723
|
+
type=click.Choice(["yes", "no", "auto"]),
|
|
724
|
+
default="auto",
|
|
725
|
+
show_default=True,
|
|
726
|
+
help="On package install: yes, force a restart | no, do not restart | auto, only restart if the package has any .py files or if there are dependencies to be installed",
|
|
727
|
+
)
|
|
538
728
|
def install(directory, config, api, server, playground, restart):
|
|
539
729
|
"""
|
|
540
730
|
Install a docassemble package on a docassemble server.
|
|
@@ -548,7 +738,13 @@ def install(directory, config, api, server, playground, restart):
|
|
|
548
738
|
else:
|
|
549
739
|
click.echo(f"""Location: Playground "{playground}" """)
|
|
550
740
|
click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Installing...""", fg="yellow")
|
|
551
|
-
package_installer(
|
|
741
|
+
package_installer(
|
|
742
|
+
directory=directory,
|
|
743
|
+
apiurl=selected_server["apiurl"],
|
|
744
|
+
apikey=selected_server["apikey"],
|
|
745
|
+
playground=playground,
|
|
746
|
+
restart=restart,
|
|
747
|
+
)
|
|
552
748
|
return 0
|
|
553
749
|
|
|
554
750
|
|
|
@@ -556,37 +752,49 @@ def install(directory, config, api, server, playground, restart):
|
|
|
556
752
|
# watchdog & hashlib
|
|
557
753
|
# -----------------------------------------------------------------------------
|
|
558
754
|
|
|
559
|
-
|
|
755
|
+
|
|
756
|
+
def calculate_checksum(filepath: str) -> str:
|
|
560
757
|
hash_md5 = hashlib.md5()
|
|
561
758
|
try:
|
|
562
759
|
with open(filepath, "rb") as f:
|
|
563
|
-
|
|
760
|
+
while chunk := f.read(4096):
|
|
564
761
|
hash_md5.update(chunk)
|
|
565
|
-
except FileNotFoundError:
|
|
762
|
+
except (FileNotFoundError, PermissionError) as e:
|
|
763
|
+
click.secho(f"""{e} while calculating checksum.""", fg="red")
|
|
566
764
|
return ""
|
|
567
765
|
return hash_md5.hexdigest()
|
|
568
766
|
|
|
569
767
|
|
|
570
768
|
def scan_directory(directory):
|
|
769
|
+
if DEBUG:
|
|
770
|
+
click.secho("Scanning files...", fg="cyan")
|
|
571
771
|
global FILE_CHECKSUMS
|
|
572
|
-
for
|
|
772
|
+
for current_directory, subdirectories, files in os.walk(directory):
|
|
773
|
+
excluded_directories = EXCLUDED_DIRECTORIES
|
|
774
|
+
subdirectories[:] = [d for d in subdirectories if d not in excluded_directories]
|
|
573
775
|
for file in files:
|
|
574
|
-
filepath = os.path.join(
|
|
776
|
+
filepath = os.path.join(current_directory, file)
|
|
575
777
|
if not matches_ignore_patterns(path=filepath, directory=directory):
|
|
576
|
-
FILE_CHECKSUMS[filepath] =
|
|
778
|
+
FILE_CHECKSUMS[filepath] = calculate_checksum(filepath)
|
|
779
|
+
if DEBUG:
|
|
780
|
+
click.secho("Scanning complete.", fg="green")
|
|
577
781
|
|
|
578
782
|
|
|
579
783
|
def matches_ignore_patterns(path: str, directory: str) -> bool:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
784
|
+
global GITMATCH_COMPILED
|
|
785
|
+
if not GITMATCH_COMPILED:
|
|
786
|
+
if DEBUG:
|
|
787
|
+
click.echo("GITMATCH_COMPILED")
|
|
788
|
+
if os.path.exists(gitignore_path := os.path.join(directory, ".gitignore")):
|
|
789
|
+
with open(gitignore_path) as file:
|
|
790
|
+
ignore_patterns = [line.strip() for line in file]
|
|
791
|
+
else:
|
|
792
|
+
ignore_patterns = GITIGNORE.split("\n")
|
|
793
|
+
ignore_patterns.extend([".git/", ".gitignore"])
|
|
794
|
+
GITMATCH_COMPILED = gitmatch.compile(ignore_patterns)
|
|
587
795
|
# Convert the absolute path to a relative path for gitmatch to work
|
|
588
796
|
path = os.path.relpath(path, directory)
|
|
589
|
-
return
|
|
797
|
+
return GITMATCH_COMPILED.match(path=path)
|
|
590
798
|
|
|
591
799
|
|
|
592
800
|
class WatchHandler(FileSystemEventHandler):
|
|
@@ -600,8 +808,10 @@ class WatchHandler(FileSystemEventHandler):
|
|
|
600
808
|
return None
|
|
601
809
|
if event.event_type == "created" or event.event_type == "modified":
|
|
602
810
|
if not matches_ignore_patterns(path=event.src_path.replace("\\", "/"), directory=self.directory):
|
|
603
|
-
new_checksum =
|
|
604
|
-
if event.src_path not in FILE_CHECKSUMS or (
|
|
811
|
+
new_checksum = calculate_checksum(event.src_path)
|
|
812
|
+
if event.src_path not in FILE_CHECKSUMS or (
|
|
813
|
+
new_checksum and FILE_CHECKSUMS[event.src_path] != new_checksum
|
|
814
|
+
):
|
|
605
815
|
FILE_CHECKSUMS[event.src_path] = new_checksum
|
|
606
816
|
LAST_MODIFIED["time"] = time.time()
|
|
607
817
|
LAST_MODIFIED["files"][str(event.src_path)] = True
|
|
@@ -613,16 +823,33 @@ class WatchHandler(FileSystemEventHandler):
|
|
|
613
823
|
# watch
|
|
614
824
|
# =============================================================================
|
|
615
825
|
|
|
826
|
+
|
|
616
827
|
@cli.command(context_settings=CONTEXT_SETTINGS)
|
|
617
828
|
@common_params_for_installation
|
|
618
829
|
@common_params_for_api
|
|
619
|
-
@click.option(
|
|
620
|
-
|
|
830
|
+
@click.option(
|
|
831
|
+
"--restart",
|
|
832
|
+
"-r",
|
|
833
|
+
type=click.Choice(["yes", "no", "auto"]),
|
|
834
|
+
default="auto",
|
|
835
|
+
show_default=True,
|
|
836
|
+
help="On package install: yes, force a restart | no, do not restart | auto, only restart if any .py files were changed",
|
|
837
|
+
)
|
|
838
|
+
@click.option(
|
|
839
|
+
"--buffer",
|
|
840
|
+
"-b",
|
|
841
|
+
metavar="SECONDS",
|
|
842
|
+
default=3,
|
|
843
|
+
show_default=True,
|
|
844
|
+
help="(On server restart only) Set the buffer (wait time) between a file change event and package installation. If you are experiencing multiple installs back-to-back, try increasing this value.",
|
|
845
|
+
)
|
|
621
846
|
def watch(directory, config, api, server, playground, restart, buffer):
|
|
622
847
|
"""
|
|
623
848
|
Watch a package directory and `install` any changes. Press Ctrl + c to exit.
|
|
849
|
+
|
|
850
|
+
If the --directory option is not specified, `watch` will look for a directory entry in the config file. The corresponding server entry will be selected automatically if the "directory" key in the config file matches the directory being watched. If a match is found, the "playground" key in the config file will be used if it exists and if no --playground option was specified.
|
|
624
851
|
"""
|
|
625
|
-
selected_server = select_server(*config, *api, server)
|
|
852
|
+
selected_server = select_server(*config, *api, server, directory=directory)
|
|
626
853
|
restart_param = restart
|
|
627
854
|
scan_directory(directory)
|
|
628
855
|
global LAST_MODIFIED
|
|
@@ -632,10 +859,30 @@ def watch(directory, config, api, server, playground, restart, buffer):
|
|
|
632
859
|
observer.start()
|
|
633
860
|
click.echo()
|
|
634
861
|
click.echo(f"""Server: {selected_server["name"]}""")
|
|
862
|
+
|
|
635
863
|
if not playground:
|
|
636
|
-
|
|
637
|
-
|
|
864
|
+
if (
|
|
865
|
+
"directory" in selected_server
|
|
866
|
+
and selected_server["directory"] == directory
|
|
867
|
+
and "playground" in selected_server
|
|
868
|
+
):
|
|
869
|
+
playground = selected_server["playground"]
|
|
870
|
+
else:
|
|
871
|
+
click.echo("Location: Package")
|
|
872
|
+
if playground:
|
|
638
873
|
click.echo(f"""Location: Playground "{playground}" """)
|
|
874
|
+
|
|
875
|
+
if "startup" in selected_server and selected_server["startup"] == "install":
|
|
876
|
+
click.secho("""Installing on startup.""", fg="cyan")
|
|
877
|
+
package_installer(
|
|
878
|
+
directory=directory,
|
|
879
|
+
apiurl=selected_server["apiurl"],
|
|
880
|
+
apikey=selected_server["apikey"],
|
|
881
|
+
playground=playground,
|
|
882
|
+
restart=restart,
|
|
883
|
+
)
|
|
884
|
+
click.echo("")
|
|
885
|
+
|
|
639
886
|
click.echo(f"""Watching: {directory}""")
|
|
640
887
|
click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Started""", fg="green")
|
|
641
888
|
try:
|
|
@@ -652,20 +899,27 @@ def watch(directory, config, api, server, playground, restart, buffer):
|
|
|
652
899
|
LAST_MODIFIED["time"] = 0
|
|
653
900
|
LAST_MODIFIED["files"] = {}
|
|
654
901
|
LAST_MODIFIED["restart"] = False
|
|
655
|
-
package_installer(
|
|
902
|
+
package_installer(
|
|
903
|
+
directory=directory,
|
|
904
|
+
apiurl=selected_server["apiurl"],
|
|
905
|
+
apikey=selected_server["apikey"],
|
|
906
|
+
playground=playground,
|
|
907
|
+
restart=restart,
|
|
908
|
+
)
|
|
656
909
|
time.sleep(1)
|
|
657
910
|
except Exception as e:
|
|
658
911
|
click.echo(f"\nException occurred: {e}")
|
|
659
912
|
finally:
|
|
660
913
|
observer.stop()
|
|
661
914
|
observer.join()
|
|
662
|
-
return
|
|
915
|
+
return """\nStopping "docassemblecli3 watch"."""
|
|
663
916
|
|
|
664
917
|
|
|
665
918
|
# =============================================================================
|
|
666
919
|
# create
|
|
667
920
|
# =============================================================================
|
|
668
921
|
|
|
922
|
+
|
|
669
923
|
@cli.command(context_settings=CONTEXT_SETTINGS)
|
|
670
924
|
@click.option("--package", metavar="PACKAGE", help="Name of the package you want to create")
|
|
671
925
|
@click.option("--developer-name", metavar="NAME", help="Name of the developer of the package")
|
|
@@ -681,10 +935,10 @@ def create(package, developer_name, developer_email, description, url, license,
|
|
|
681
935
|
"""
|
|
682
936
|
pkgname = package
|
|
683
937
|
if not pkgname:
|
|
684
|
-
|
|
938
|
+
pkgname = click.prompt("Name of the package you want to create (e.g., childsupport)")
|
|
685
939
|
pkgname = re.sub(r"\s", "", pkgname)
|
|
686
940
|
if not pkgname:
|
|
687
|
-
return
|
|
941
|
+
return "The package name you entered is invalid."
|
|
688
942
|
pkgname = re.sub(r"^docassemble[\-\.]", "", pkgname, flags=re.IGNORECASE)
|
|
689
943
|
if output:
|
|
690
944
|
packagedir = output
|
|
@@ -692,10 +946,10 @@ def create(package, developer_name, developer_email, description, url, license,
|
|
|
692
946
|
packagedir = "docassemble-" + pkgname
|
|
693
947
|
if os.path.exists(packagedir):
|
|
694
948
|
if not os.path.isdir(packagedir):
|
|
695
|
-
return
|
|
949
|
+
return "Cannot create the directory " + packagedir + " because the path already exists."
|
|
696
950
|
dir_listing = list(os.listdir(packagedir))
|
|
697
951
|
if "setup.py" in dir_listing or "setup.cfg" in dir_listing:
|
|
698
|
-
return
|
|
952
|
+
return "The directory " + packagedir + " already has a package in it."
|
|
699
953
|
else:
|
|
700
954
|
os.makedirs(packagedir, exist_ok=True)
|
|
701
955
|
if not developer_name:
|
|
@@ -724,7 +978,12 @@ __import__("pkg_resources").declare_namespace(__name__)
|
|
|
724
978
|
|
|
725
979
|
"""
|
|
726
980
|
if "MIT" in license:
|
|
727
|
-
licensetext =
|
|
981
|
+
licensetext = (
|
|
982
|
+
"The MIT License (MIT)\n\nCopyright (c) "
|
|
983
|
+
+ str(datetime.datetime.now().year)
|
|
984
|
+
+ " "
|
|
985
|
+
+ developer_name
|
|
986
|
+
+ """
|
|
728
987
|
|
|
729
988
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
730
989
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -744,10 +1003,21 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
744
1003
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
745
1004
|
SOFTWARE.
|
|
746
1005
|
"""
|
|
1006
|
+
)
|
|
747
1007
|
else:
|
|
748
1008
|
licensetext = license + "\n"
|
|
749
1009
|
|
|
750
|
-
readme =
|
|
1010
|
+
readme = (
|
|
1011
|
+
"# docassemble."
|
|
1012
|
+
+ pkgname
|
|
1013
|
+
+ "\n\n"
|
|
1014
|
+
+ description
|
|
1015
|
+
+ "\n\n## Author\n\n"
|
|
1016
|
+
+ developer_name
|
|
1017
|
+
+ ", "
|
|
1018
|
+
+ developer_email
|
|
1019
|
+
+ "\n"
|
|
1020
|
+
)
|
|
751
1021
|
manifestin = """\
|
|
752
1022
|
include README.md
|
|
753
1023
|
"""
|
|
@@ -802,22 +1072,44 @@ def find_package_data(where=".", package="", exclude=standard_exclude, exclude_d
|
|
|
802
1072
|
return out
|
|
803
1073
|
|
|
804
1074
|
"""
|
|
805
|
-
setuppy +=
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1075
|
+
setuppy += (
|
|
1076
|
+
"setup(name="
|
|
1077
|
+
+ repr("docassemble." + pkgname)
|
|
1078
|
+
+ """,
|
|
1079
|
+
version="""
|
|
1080
|
+
+ repr(version)
|
|
1081
|
+
+ """,
|
|
1082
|
+
description=("""
|
|
1083
|
+
+ repr(description)
|
|
1084
|
+
+ """),
|
|
1085
|
+
long_description="""
|
|
1086
|
+
+ repr(readme)
|
|
1087
|
+
+ """,
|
|
809
1088
|
long_description_content_type="text/markdown",
|
|
810
|
-
author="""
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1089
|
+
author="""
|
|
1090
|
+
+ repr(developer_name)
|
|
1091
|
+
+ """,
|
|
1092
|
+
author_email="""
|
|
1093
|
+
+ repr(developer_email)
|
|
1094
|
+
+ """,
|
|
1095
|
+
license="""
|
|
1096
|
+
+ repr(license)
|
|
1097
|
+
+ """,
|
|
1098
|
+
url="""
|
|
1099
|
+
+ repr(package_url)
|
|
1100
|
+
+ """,
|
|
814
1101
|
packages=find_packages(),
|
|
815
1102
|
namespace_packages=["docassemble"],
|
|
816
1103
|
install_requires=[],
|
|
817
1104
|
zip_safe=False,
|
|
818
|
-
package_data=find_package_data(where='docassemble/"""
|
|
1105
|
+
package_data=find_package_data(where='docassemble/"""
|
|
1106
|
+
+ pkgname
|
|
1107
|
+
+ """/', package='docassemble."""
|
|
1108
|
+
+ pkgname
|
|
1109
|
+
+ """'),
|
|
819
1110
|
)
|
|
820
1111
|
"""
|
|
1112
|
+
)
|
|
821
1113
|
# maindir = os.path.join(packagedir, "docassemble", pkgname)
|
|
822
1114
|
questionsdir = os.path.join(packagedir, "docassemble", pkgname, "data", "questions")
|
|
823
1115
|
templatesdir = os.path.join(packagedir, "docassemble", pkgname, "data", "templates")
|
|
@@ -831,7 +1123,7 @@ def find_package_data(where=".", package="", exclude=standard_exclude, exclude_d
|
|
|
831
1123
|
os.makedirs(staticdir, exist_ok=True)
|
|
832
1124
|
if not os.path.isdir(sourcesdir):
|
|
833
1125
|
os.makedirs(sourcesdir, exist_ok=True)
|
|
834
|
-
with open(os.path.join(packagedir,
|
|
1126
|
+
with open(os.path.join(packagedir, ".gitignore"), "w", encoding="utf-8") as the_file:
|
|
835
1127
|
the_file.write(GITIGNORE)
|
|
836
1128
|
with open(os.path.join(packagedir, "README.md"), "w", encoding="utf-8") as the_file:
|
|
837
1129
|
the_file.write(readme)
|
|
@@ -854,18 +1146,18 @@ def find_package_data(where=".", package="", exclude=standard_exclude, exclude_d
|
|
|
854
1146
|
# config
|
|
855
1147
|
# =============================================================================
|
|
856
1148
|
|
|
1149
|
+
|
|
857
1150
|
@config.command(context_settings=CONTEXT_SETTINGS)
|
|
858
1151
|
@common_params_for_config
|
|
859
|
-
@
|
|
860
|
-
|
|
1152
|
+
@common_params_for_api
|
|
1153
|
+
@common_params_for_installation
|
|
1154
|
+
def add(config, api, directory, playground):
|
|
861
1155
|
"""
|
|
862
1156
|
Add a server to the config file.
|
|
863
1157
|
"""
|
|
864
1158
|
apiurl, apikey = api
|
|
865
|
-
if not apiurl or not apikey:
|
|
866
|
-
apiurl, apikey = prompt_for_api(previous_url=apiurl, previous_key=apikey)
|
|
867
1159
|
cfg, env = config
|
|
868
|
-
add_server_to_env(cfg=cfg, env=env, apiurl=apiurl, apikey=apikey)
|
|
1160
|
+
add_server_to_env(cfg=cfg, env=env, apiurl=apiurl, apikey=apikey, directory=directory, playground=playground)
|
|
869
1161
|
|
|
870
1162
|
|
|
871
1163
|
@config.command(context_settings=CONTEXT_SETTINGS)
|
|
@@ -924,7 +1216,9 @@ def new(config):
|
|
|
924
1216
|
def server_version(config, api, server):
|
|
925
1217
|
selected_server = select_server(*config, *api, server)
|
|
926
1218
|
try:
|
|
927
|
-
r = requests.get(
|
|
1219
|
+
r = requests.get(
|
|
1220
|
+
selected_server["apiurl"] + "/api/package", headers={"X-API-Key": selected_server["apikey"]}, timeout=600
|
|
1221
|
+
)
|
|
928
1222
|
if DEBUG:
|
|
929
1223
|
click.echo(type(r.status_code))
|
|
930
1224
|
click.echo(r.status_code)
|
|
@@ -953,4 +1247,3 @@ def test(config, api, server):
|
|
|
953
1247
|
apikey = selected_server["apikey"]
|
|
954
1248
|
click.echo(apiurl)
|
|
955
1249
|
test_apiurl_apikey(apiurl=apiurl, apikey=apikey)
|
|
956
|
-
|
|
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "docassemblecli3"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.6"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "Jack Adamson", email = "jackadamson@gmail.com"},
|
|
10
10
|
{name = "Jonathan Pyle", email = "jhpyle@gmail.com"},
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python",
|
|
17
17
|
]
|
|
18
18
|
dynamic = ["description"]
|
|
19
|
-
requires-python = ">= 3.
|
|
19
|
+
requires-python = ">= 3.10"
|
|
20
20
|
keywords = ["docassemble"]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"click",
|
|
@@ -24,7 +24,7 @@ dependencies = [
|
|
|
24
24
|
"packaging",
|
|
25
25
|
"PyYAML",
|
|
26
26
|
"requests",
|
|
27
|
-
"watchdog"
|
|
27
|
+
"watchdog",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|