docassemblecli3 0.2.2__tar.gz → 0.3.3__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.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: docassemblecli3
3
- Version: 0.2.2
3
+ Version: 0.3.3
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.8
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.8 installed on your
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\jacka\.docassemblecli]
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\jacka\.docassemblecli]
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 manually add a `path` key to a server in your `.docassemblecli` config
289
+ file, it will cause that server to be used if no `server` is provided and the
290
+ `path` matches the `directory` that `watch` was given
291
+ [default: current directory]. Additionally, if you manually add a `playground`
292
+ key to that server, it will be used when using `watch`.
286
293
 
287
294
  #### watchdog
288
295
 
289
- The `watch` command now depends on the
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
 
@@ -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.8 installed on your
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\jacka\.docassemblecli]
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\jacka\.docassemblecli]
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 manually add a `path` key to a server in your `.docassemblecli` config
270
+ file, it will cause that server to be used if no `server` is provided and the
271
+ `path` matches the `directory` that `watch` was given
272
+ [default: current directory]. Additionally, if you manually add a `playground`
273
+ key to that server, it will be used when using `watch`.
267
274
 
268
275
  #### watchdog
269
276
 
270
- The `watch` command now depends on the
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
 
@@ -34,6 +34,12 @@ FILE_CHECKSUMS = {}
34
34
  global DEBUG
35
35
  DEBUG = False
36
36
 
37
+ global EXCLUDED_DIRECTORIES
38
+ EXCLUDED_DIRECTORIES = [".git", "__pycache__", ".mypy_cache", ".venv", ".history", "build"]
39
+
40
+ global GITMATCH_COMPILED
41
+ GITMATCH_COMPILED = None
42
+
37
43
  global GITIGNORE
38
44
  GITIGNORE = """\
39
45
  __pycache__/
@@ -92,7 +98,13 @@ CONTEXT_SETTINGS = dict(help_option_names=["--help", "-h"])
92
98
 
93
99
  @click.group(context_settings=CONTEXT_SETTINGS)
94
100
  @click.version_option()
95
- @click.option("--color/--no-color", "-C/-N", default=None, show_default=True, help="Overrides color auto-detection in interactive terminals.")
101
+ @click.option(
102
+ "--color/--no-color",
103
+ "-C/-N",
104
+ default=None,
105
+ show_default=True,
106
+ help="Overrides color auto-detection in interactive terminals.",
107
+ )
96
108
  @click.option("--debug/--no-debug", default=False, hidden=True)
97
109
  def cli(color, debug):
98
110
  """
@@ -113,34 +125,76 @@ def config():
113
125
 
114
126
 
115
127
  def common_params_for_api(func):
116
- @click.option("--api", "-a", type=(APIURLType(), str), default=(None, None), help="URL of the docassemble server and API key of the user (admin or developer)")
128
+ @click.option(
129
+ "--api",
130
+ "-a",
131
+ type=(APIURLType(), str),
132
+ default=(None, None),
133
+ help="URL of the docassemble server and API key of the user (admin or developer)",
134
+ )
117
135
  @click.option("--server", "-s", metavar="SERVER", default="", help="Specify a server from the config file")
118
136
  @wraps(func)
119
137
  def wrapper(*args, **kwargs):
120
138
  return func(*args, **kwargs)
139
+
121
140
  return wrapper
122
141
 
123
142
 
124
143
  def common_params_for_config(func):
125
- @click.option("--config", "-c", default=DEFAULT_CONFIG, type=click.Path(), callback=validate_and_load_or_create_config, show_default=True, help="Specify the config file to use")
144
+ @click.option(
145
+ "--config",
146
+ "-c",
147
+ default=DEFAULT_CONFIG,
148
+ type=click.Path(),
149
+ callback=validate_and_load_or_create_config,
150
+ show_default=True,
151
+ help="Specify the config file to use",
152
+ )
126
153
  @wraps(func)
127
154
  def wrapper(*args, **kwargs):
128
155
  return func(*args, **kwargs)
156
+
129
157
  return wrapper
130
158
 
131
159
 
132
160
  def common_params_for_installation(func):
133
- @click.option("--directory", "-d", default=os.getcwd(), type=click.Path(), callback=validate_package_directory, help="Specify package directory [default: current directory]")
134
- @click.option("--config", "-c", is_flag=False, flag_value="", default=DEFAULT_CONFIG, type=click.Path(), callback=validate_and_load_or_create_config, show_default=True, help="Specify the config file to use or leave it blank to skip using any config file")
135
- @click.option("--playground", "-p", metavar="(PROJECT)", is_flag=False, flag_value="default", help="Install into the default Playground or into the specified Playground project.")
161
+ @click.option(
162
+ "--directory",
163
+ "-d",
164
+ default=os.getcwd(),
165
+ type=click.Path(),
166
+ callback=validate_package_directory,
167
+ help="Specify package directory [default: current directory]",
168
+ )
169
+ @click.option(
170
+ "--config",
171
+ "-c",
172
+ is_flag=False,
173
+ flag_value="",
174
+ default=DEFAULT_CONFIG,
175
+ type=click.Path(),
176
+ callback=validate_and_load_or_create_config,
177
+ show_default=True,
178
+ help="Specify the config file to use or leave it blank to skip using any config file",
179
+ )
180
+ @click.option(
181
+ "--playground",
182
+ "-p",
183
+ metavar="(PROJECT)",
184
+ is_flag=False,
185
+ flag_value="default",
186
+ help="Install into the default Playground or into the specified Playground project.",
187
+ )
136
188
  @wraps(func)
137
189
  def wrapper(*args, **kwargs):
138
190
  return func(*args, **kwargs)
191
+
139
192
  return wrapper
140
193
 
141
194
 
142
195
  class APIURLType(click.ParamType):
143
196
  name = "url"
197
+
144
198
  def convert(self, value, param, ctx):
145
199
  parsed_url = urlparse(value)
146
200
  if all([re.search(r"""^https?://[^\s]+$""", value), parsed_url.scheme, parsed_url.netloc]):
@@ -154,7 +208,9 @@ def validate_package_directory(ctx, param, directory: str) -> str:
154
208
  if not os.path.exists(directory):
155
209
  raise click.BadParameter(f"""Directory "{directory}" does not exist.""")
156
210
  if not os.path.isfile(os.path.join(directory, "setup.py")):
157
- raise click.BadParameter(f"""Directory "{directory}" does not contain a setup.py file, so it is not the directory of a valid Python package.""")
211
+ raise click.BadParameter(
212
+ f"""Directory "{directory}" does not contain a setup.py file, so it is not the directory of a valid Python package."""
213
+ )
158
214
  else:
159
215
  return directory
160
216
 
@@ -185,6 +241,7 @@ def validate_and_load_or_create_config(ctx, param, config: str) -> tuple[str, li
185
241
  # utility functions
186
242
  # -----------------------------------------------------------------------------
187
243
 
244
+
188
245
  def name_from_url(url: str) -> str:
189
246
  if not url:
190
247
  return ""
@@ -203,7 +260,7 @@ def display_servers(env: list = None) -> list[str]:
203
260
  return servers
204
261
 
205
262
 
206
- def select_server(cfg: str = None, env: list = None, apiurl: str = None, apikey: str = None, server: str = "") -> dict:
263
+ def select_server(cfg: str = None, env: list = None, apiurl: str = None, apikey: str = None, server: str = "", **kwargs) -> dict:
207
264
  if apiurl and apikey:
208
265
  return add_server_to_env(cfg=cfg, env=env, apiurl=apiurl, apikey=apikey)[-1]
209
266
  if isinstance(env, list):
@@ -216,6 +273,10 @@ def select_server(cfg: str = None, env: list = None, apiurl: str = None, apikey:
216
273
  return item
217
274
  raise click.BadParameter(f"""Server "{server}" was not found.""", param_hint="--server")
218
275
  if len(env) > 0:
276
+ if "watch" in kwargs:
277
+ for item in env:
278
+ if item.get("path", None) == kwargs["watch"]:
279
+ return item
219
280
  return env[0]
220
281
  if "DOCASSEMBLEAPIURL" in os.environ and "DOCASSEMBLEAPIKEY" in os.environ:
221
282
  apiurl: str = os.environ["DOCASSEMBLEAPIURL"]
@@ -256,7 +317,11 @@ def prompt_for_api(retry: str = False, previous_url: str = None, previous_key: s
256
317
  if retry:
257
318
  if not click.confirm("Do you want to try another URL and API key?", default=True):
258
319
  raise click.Abort()
259
- apiurl = click.prompt("""Base URL of your docassemble server (e.g., https://da.example.com)""", type=APIURLType(), default=previous_url)
320
+ apiurl = click.prompt(
321
+ """Base URL of your docassemble server (e.g., https://da.example.com)""",
322
+ type=APIURLType(),
323
+ default=previous_url,
324
+ )
260
325
  apikey = click.prompt(f"""API key of admin or developer user on {apiurl}""", default=previous_key).strip()
261
326
  return apiurl, apikey
262
327
 
@@ -267,9 +332,13 @@ def test_apiurl_apikey(apiurl: str, apikey: str) -> bool:
267
332
  api_test = requests.get(apiurl + "/api/package", headers={"X-API-Key": apikey})
268
333
  if api_test.status_code != 200:
269
334
  if api_test.status_code == 403:
270
- click.secho(f"""\nThe API KEY is invalid. ({api_test.status_code} {api_test.text.strip()})\n""", fg="red")
335
+ click.secho(
336
+ f"""\nThe API KEY is invalid. ({api_test.status_code} {api_test.text.strip()})\n""", fg="red"
337
+ )
271
338
  else:
272
- click.secho(f"""\nThe API URL or KEY is invalid. ({api_test.status_code} {api_test.text.strip()})\n""", fg="red")
339
+ click.secho(
340
+ f"""\nThe API URL or KEY is invalid. ({api_test.status_code} {api_test.text.strip()})\n""", fg="red"
341
+ )
273
342
  return False
274
343
  except Exception as err:
275
344
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
@@ -291,14 +360,14 @@ def add_server_to_env(cfg: str = None, env: list = None, apiurl: str = None, api
291
360
  return env
292
361
 
293
362
 
294
- def select_env(cfg: str = None, env: list = None, apiurl: str = None, apikey: str = None, server: str = None) -> dict:
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)
363
+ # def select_env(cfg: str = None, env: list = None, apiurl: str = None, apikey: str = None, server: str = None) -> dict:
364
+ # if apiurl and apikey:
365
+ # return add_server_to_env(cfg=cfg, env=env, apiurl=apiurl, apikey=apikey)[-1]
366
+ # else:
367
+ # return select_server(cfg=cfg, env=env, server=server)
299
368
 
300
369
 
301
- def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, server_version_da: str = "0"):
370
+ def wait_for_server(playground: bool, task_id: str, apikey: str, apiurl: str, server_version_da: str = "0"):
302
371
  click.secho("Waiting for package to install...", fg="cyan")
303
372
  tries = 0
304
373
  before_wait_for_server = time.time()
@@ -312,7 +381,7 @@ def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, ser
312
381
  except requests.exceptions.RequestException:
313
382
  pass
314
383
  if r.status_code != 200:
315
- return("package_update_status returned " + str(r.status_code) + ": " + r.text)
384
+ return "package_update_status returned " + str(r.status_code) + ": " + r.text
316
385
  info = r.json()
317
386
  if info["status"] == "completed" or info["status"] == "unknown":
318
387
  break
@@ -325,7 +394,10 @@ def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, ser
325
394
  success = True
326
395
  elif info.get("ok", False):
327
396
  success = True
328
- if not (server_version_da == "norestart" or packaging_version.parse(server_version_da) >= packaging_version.parse("1.5.3")):
397
+ if not (
398
+ server_version_da == "norestart"
399
+ or packaging_version.parse(server_version_da) >= packaging_version.parse("1.5.3")
400
+ ):
329
401
  if DEBUG:
330
402
  click.echo(f"""Package install duration: {(after_wait_for_server - before_wait_for_server):.2f}s""")
331
403
  click.echo("""Manually waiting for background processes.""")
@@ -345,11 +417,19 @@ def wait_for_server(playground:bool, task_id: str, apikey: str, apiurl: str, ser
345
417
  # package_installer
346
418
  # -----------------------------------------------------------------------------
347
419
 
420
+
348
421
  def package_installer(directory, apiurl, apikey, playground, restart):
349
422
  archive = tempfile.NamedTemporaryFile(suffix=".zip")
350
423
  zf = zipfile.ZipFile(archive, compression=zipfile.ZIP_DEFLATED, mode="w")
351
424
  try:
352
- ignore_process = subprocess.run(["git", "ls-files", "-i", "--directory", "-o", "--exclude-standard"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, cwd=directory, check=False)
425
+ ignore_process = subprocess.run(
426
+ ["git", "ls-files", "-i", "--directory", "-o", "--exclude-standard"],
427
+ stdout=subprocess.PIPE,
428
+ stderr=subprocess.PIPE,
429
+ universal_newlines=True,
430
+ cwd=directory,
431
+ check=False,
432
+ )
353
433
  ignore_process.check_returncode()
354
434
  raw_ignore = ignore_process.stdout.splitlines()
355
435
  except Exception:
@@ -361,7 +441,13 @@ def package_installer(directory, apiurl, apikey, playground, restart):
361
441
  dependencies = {}
362
442
  for root, dirs, files in os.walk(directory, topdown=True):
363
443
  adjusted_root = os.sep.join(root.split(os.sep)[1:])
364
- dirs[:] = [d for d in dirs if d not in [".git", "__pycache__", ".mypy_cache", ".venv", ".history", "build"] and not d.endswith(".egg-info") and os.path.join(adjusted_root, d) not in to_ignore]
444
+ dirs[:] = [
445
+ d
446
+ for d in dirs
447
+ if d not in EXCLUDED_DIRECTORIES
448
+ and not d.endswith(".egg-info")
449
+ and os.path.join(adjusted_root, d) not in to_ignore
450
+ ]
365
451
  if root_directory is None and ("setup.py" in files or "setup.cfg" in files):
366
452
  root_directory = root
367
453
  if "setup.py" in files:
@@ -374,24 +460,48 @@ def package_installer(directory, apiurl, apikey, playground, restart):
374
460
  if m:
375
461
  for package_text in m.group(1).split(","):
376
462
  package_name = package_text.strip()
377
- if len(package_name) >= 3 and package_name[0] == package_name[-1] and package_name[0] in (""", """):
463
+ if (
464
+ len(package_name) >= 3
465
+ and package_name[0] == package_name[-1]
466
+ and package_name[0] in (""", """)
467
+ ):
378
468
  package_name = package_name[1:-1]
379
469
  mm = re.search(r"""(.*)(<=|>=|==|<|>)(.*)""", package_name)
380
470
  if mm:
381
- dependencies[mm.group(1).strip()] = {"installed": False, "operator": mm.group(2), "version": mm.group(3).strip()}
471
+ dependencies[mm.group(1).strip()] = {
472
+ "installed": False,
473
+ "operator": mm.group(2),
474
+ "version": mm.group(3).strip(),
475
+ }
382
476
  else:
383
477
  dependencies[package_name] = {"installed": False, "operator": None, "version": None}
384
478
  for the_file in files:
385
- if the_file.endswith("~") or the_file.endswith(".pyc") or the_file.endswith(".swp") or the_file.startswith("#") or the_file.startswith(".#") or (the_file == ".gitignore" and root_directory == root) or os.path.join(adjusted_root, the_file) in to_ignore:
479
+ if (
480
+ the_file.endswith("~")
481
+ or the_file.endswith(".pyc")
482
+ or the_file.endswith(".swp")
483
+ or the_file.startswith("#")
484
+ or the_file.startswith(".#")
485
+ or (the_file == ".gitignore" and root_directory == root)
486
+ or os.path.join(adjusted_root, the_file) in to_ignore
487
+ ):
386
488
  continue
387
- if not has_python_files and the_file.endswith(".py") and not (the_file == "setup.py" and root == root_directory) and the_file != "__init__.py":
489
+ if (
490
+ not has_python_files
491
+ and the_file.endswith(".py")
492
+ and not (the_file == "setup.py" and root == root_directory)
493
+ and the_file != "__init__.py"
494
+ ):
388
495
  has_python_files = True
389
- zf.write(os.path.join(root, the_file), os.path.relpath(os.path.join(root, the_file), os.path.join(directory, "..")))
496
+ zf.write(
497
+ os.path.join(root, the_file),
498
+ os.path.relpath(os.path.join(root, the_file), os.path.join(directory, "..")),
499
+ )
390
500
  zf.close()
391
501
  archive.seek(0)
392
502
  if restart == "no":
393
503
  should_restart = False
394
- elif restart =="yes" or has_python_files:
504
+ elif restart == "yes" or has_python_files:
395
505
  should_restart = True
396
506
  elif len(dependencies) > 0 or this_package_name:
397
507
  try:
@@ -400,7 +510,7 @@ def package_installer(directory, apiurl, apikey, playground, restart):
400
510
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
401
511
  raise click.ClickException(f"""{err}\n""")
402
512
  if r.status_code != 200:
403
- return("/api/package returned " + str(r.status_code) + ": " + r.text)
513
+ return "/api/package returned " + str(r.status_code) + ": " + r.text
404
514
  installed_packages = r.json()
405
515
  already_installed = False
406
516
  for package_info in installed_packages:
@@ -410,20 +520,33 @@ def package_installer(directory, apiurl, apikey, playground, restart):
410
520
  condition = True
411
521
  if dependency_info["operator"]:
412
522
  if dependency_info["operator"] == "==":
413
- condition = packaging_version.parse(package_info["version"]) == packaging_version.parse(dependency_info["version"])
523
+ condition = packaging_version.parse(package_info["version"]) == packaging_version.parse(
524
+ dependency_info["version"]
525
+ )
414
526
  elif dependency_info["operator"] == "<=":
415
- condition = packaging_version.parse(package_info["version"]) <= packaging_version.parse(dependency_info["version"])
527
+ condition = packaging_version.parse(package_info["version"]) <= packaging_version.parse(
528
+ dependency_info["version"]
529
+ )
416
530
  elif dependency_info["operator"] == ">=":
417
- condition = packaging_version.parse(package_info["version"]) >= packaging_version.parse(dependency_info["version"])
531
+ condition = packaging_version.parse(package_info["version"]) >= packaging_version.parse(
532
+ dependency_info["version"]
533
+ )
418
534
  elif dependency_info["operator"] == "<":
419
- condition = packaging_version.parse(package_info["version"]) < packaging_version.parse(dependency_info["version"])
535
+ condition = packaging_version.parse(package_info["version"]) < packaging_version.parse(
536
+ dependency_info["version"]
537
+ )
420
538
  elif dependency_info["operator"] == ">":
421
- condition = packaging_version.parse(package_info["version"]) > packaging_version.parse(dependency_info["version"])
539
+ condition = packaging_version.parse(package_info["version"]) > packaging_version.parse(
540
+ dependency_info["version"]
541
+ )
422
542
  if condition:
423
543
  dependency_info["installed"] = True
424
544
  if this_package_name and this_package_name in (package_info["name"], package_info["alt_name"]):
425
545
  already_installed = True
426
- should_restart = bool((not already_installed and len(dependencies) > 0) or not all(item["installed"] for item in dependencies.values()))
546
+ should_restart = bool(
547
+ (not already_installed and len(dependencies) > 0)
548
+ or not all(item["installed"] for item in dependencies.values())
549
+ )
427
550
  else:
428
551
  should_restart = True
429
552
  data = {}
@@ -458,12 +581,20 @@ def package_installer(directory, apiurl, apikey, playground, restart):
458
581
  try:
459
582
  requests.post(project_endpoint, data={"project": playground}, headers={"X-API-Key": apikey})
460
583
  except Exception:
461
- return("create project POST returned " + project_list.text)
584
+ return "create project POST returned " + project_list.text
462
585
  else:
463
586
  click.echo("\n")
464
- return("playground list of projects GET returned " + str(project_list.status_code) + ": " + project_list.text)
587
+ return (
588
+ "playground list of projects GET returned " + str(project_list.status_code) + ": " + project_list.text
589
+ )
465
590
  try:
466
- r = requests.post(apiurl + "/api/playground_install", data=data, files={"file": archive}, headers={"X-API-Key": apikey}, timeout=600)
591
+ r = requests.post(
592
+ apiurl + "/api/playground_install",
593
+ data=data,
594
+ files={"file": archive},
595
+ headers={"X-API-Key": apikey},
596
+ timeout=600,
597
+ )
467
598
  except Exception as err:
468
599
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
469
600
  raise click.ClickException(f"""{err}\n""")
@@ -473,17 +604,33 @@ def package_installer(directory, apiurl, apikey, playground, restart):
473
604
  except Exception:
474
605
  error_message = ""
475
606
  if "project" not in data or error_message != "Invalid project.":
476
- return("playground_install POST returned " + str(r.status_code) + ": " + r.text)
607
+ return "playground_install POST returned " + str(r.status_code) + ": " + r.text
477
608
  try:
478
- r = requests.post(apiurl + "/api/playground/project", data={"project": data["project"]}, headers={"X-API-Key": apikey}, timeout=600)
609
+ r = requests.post(
610
+ apiurl + "/api/playground/project",
611
+ data={"project": data["project"]},
612
+ headers={"X-API-Key": apikey},
613
+ timeout=600,
614
+ )
479
615
  except Exception as err:
480
616
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
481
617
  raise click.ClickException(f"""{err}\n""")
482
618
  if r.status_code != 204:
483
- return("needed to create playground project but POST to api/playground/project returned " + str(r.status_code) + ": " + r.text)
619
+ return (
620
+ "needed to create playground project but POST to api/playground/project returned "
621
+ + str(r.status_code)
622
+ + ": "
623
+ + r.text
624
+ )
484
625
  archive.seek(0)
485
626
  try:
486
- r = requests.post(apiurl + "/api/playground_install", data=data, files={"file": archive}, headers={"X-API-Key": apikey}, timeout=600)
627
+ r = requests.post(
628
+ apiurl + "/api/playground_install",
629
+ data=data,
630
+ files={"file": archive},
631
+ headers={"X-API-Key": apikey},
632
+ timeout=600,
633
+ )
487
634
  except Exception as err:
488
635
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
489
636
  raise click.ClickException(f"""{err}\n""")
@@ -491,14 +638,20 @@ def package_installer(directory, apiurl, apikey, playground, restart):
491
638
  try:
492
639
  info = r.json()
493
640
  except Exception:
494
- return(r.text)
641
+ return r.text
495
642
  task_id = info["task_id"]
496
- success = wait_for_server(playground=bool(playground), task_id=task_id, apikey=apikey, apiurl=apiurl, server_version_da=server_version_da)
643
+ success = wait_for_server(
644
+ playground=bool(playground),
645
+ task_id=task_id,
646
+ apikey=apikey,
647
+ apiurl=apiurl,
648
+ server_version_da=server_version_da,
649
+ )
497
650
  elif r.status_code == 204:
498
651
  success = True
499
652
  else:
500
653
  click.echo("\n")
501
- return("playground_install POST returned " + str(r.status_code) + ": " + r.text)
654
+ return "playground_install POST returned " + str(r.status_code) + ": " + r.text
502
655
  if success:
503
656
  click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Installed.""", fg="green")
504
657
  else:
@@ -506,15 +659,23 @@ def package_installer(directory, apiurl, apikey, playground, restart):
506
659
  return 1
507
660
  else:
508
661
  try:
509
- r = requests.post(apiurl + "/api/package", data=data, files={"zip": archive}, headers={"X-API-Key": apikey}, timeout=600)
662
+ r = requests.post(
663
+ apiurl + "/api/package", data=data, files={"zip": archive}, headers={"X-API-Key": apikey}, timeout=600
664
+ )
510
665
  except Exception as err:
511
666
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
512
667
  raise click.ClickException(f"""{err}\n""")
513
668
  if r.status_code != 200:
514
- return("package POST returned " + str(r.status_code) + ": " + r.text)
669
+ return "package POST returned " + str(r.status_code) + ": " + r.text
515
670
  info = r.json()
516
671
  task_id = info["task_id"]
517
- if wait_for_server(playground=bool(playground), task_id=task_id, apikey=apikey, apiurl=apiurl, server_version_da=server_version_da):
672
+ if wait_for_server(
673
+ playground=bool(playground),
674
+ task_id=task_id,
675
+ apikey=apikey,
676
+ apiurl=apiurl,
677
+ server_version_da=server_version_da,
678
+ ):
518
679
  click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Installed.""", fg="green")
519
680
  if not should_restart:
520
681
  try:
@@ -523,7 +684,7 @@ def package_installer(directory, apiurl, apikey, playground, restart):
523
684
  click.secho(f"""\n{err.__class__.__name__}""", fg="red")
524
685
  raise click.ClickException(f"""{err}\n""")
525
686
  if r.status_code != 204:
526
- return("clear_cache returned " + str(r.status_code) + ": " + r.text)
687
+ return "clear_cache returned " + str(r.status_code) + ": " + r.text
527
688
  return 0
528
689
 
529
690
 
@@ -531,10 +692,18 @@ def package_installer(directory, apiurl, apikey, playground, restart):
531
692
  # install
532
693
  # =============================================================================
533
694
 
695
+
534
696
  @cli.command(context_settings=CONTEXT_SETTINGS)
535
697
  @common_params_for_api
536
698
  @common_params_for_installation
537
- @click.option("--restart", "-r", type=click.Choice(["yes", "no", "auto"]), default="auto", show_default=True, 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")
699
+ @click.option(
700
+ "--restart",
701
+ "-r",
702
+ type=click.Choice(["yes", "no", "auto"]),
703
+ default="auto",
704
+ show_default=True,
705
+ 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",
706
+ )
538
707
  def install(directory, config, api, server, playground, restart):
539
708
  """
540
709
  Install a docassemble package on a docassemble server.
@@ -548,7 +717,13 @@ def install(directory, config, api, server, playground, restart):
548
717
  else:
549
718
  click.echo(f"""Location: Playground "{playground}" """)
550
719
  click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Installing...""", fg="yellow")
551
- package_installer(directory=directory, apiurl=selected_server["apiurl"], apikey=selected_server["apikey"], playground=playground, restart=restart)
720
+ package_installer(
721
+ directory=directory,
722
+ apiurl=selected_server["apiurl"],
723
+ apikey=selected_server["apikey"],
724
+ playground=playground,
725
+ restart=restart,
726
+ )
552
727
  return 0
553
728
 
554
729
 
@@ -556,11 +731,12 @@ def install(directory, config, api, server, playground, restart):
556
731
  # watchdog & hashlib
557
732
  # -----------------------------------------------------------------------------
558
733
 
734
+
559
735
  def calculate_md5(filepath: str) -> str:
560
736
  hash_md5 = hashlib.md5()
561
737
  try:
562
738
  with open(filepath, "rb") as f:
563
- for chunk in iter(lambda: f.read(4096), b""):
739
+ while chunk := f.read(4096):
564
740
  hash_md5.update(chunk)
565
741
  except FileNotFoundError:
566
742
  return ""
@@ -568,25 +744,35 @@ def calculate_md5(filepath: str) -> str:
568
744
 
569
745
 
570
746
  def scan_directory(directory):
747
+ if DEBUG:
748
+ click.secho("Scanning files...", fg="cyan")
571
749
  global FILE_CHECKSUMS
572
- for root, _, files in os.walk(directory):
750
+ for current_directory, subdirectories, files in os.walk(directory):
751
+ excluded_directories = EXCLUDED_DIRECTORIES
752
+ subdirectories[:] = [d for d in subdirectories if d not in excluded_directories]
573
753
  for file in files:
574
- filepath = os.path.join(root, file)
754
+ filepath = os.path.join(current_directory, file)
575
755
  if not matches_ignore_patterns(path=filepath, directory=directory):
576
756
  FILE_CHECKSUMS[filepath] = calculate_md5(filepath)
757
+ if DEBUG:
758
+ click.secho("Scanning complete.", fg="green")
577
759
 
578
760
 
579
761
  def matches_ignore_patterns(path: str, directory: str) -> bool:
580
- if os.path.exists(gitignore_path := os.path.join(directory, ".gitignore")):
581
- with open(gitignore_path) as file:
582
- ignore_patterns = [line.strip() for line in file]
583
- else:
584
- ignore_patterns = GITIGNORE.split("\n")
585
- ignore_patterns.extend([".git/", ".gitignore"])
586
- gm = gitmatch.compile(ignore_patterns)
762
+ global GITMATCH_COMPILED
763
+ if not GITMATCH_COMPILED:
764
+ if DEBUG:
765
+ click.echo("GITMATCH_COMPILED")
766
+ if os.path.exists(gitignore_path := os.path.join(directory, ".gitignore")):
767
+ with open(gitignore_path) as file:
768
+ ignore_patterns = [line.strip() for line in file]
769
+ else:
770
+ ignore_patterns = GITIGNORE.split("\n")
771
+ ignore_patterns.extend([".git/", ".gitignore"])
772
+ GITMATCH_COMPILED = gitmatch.compile(ignore_patterns)
587
773
  # Convert the absolute path to a relative path for gitmatch to work
588
774
  path = os.path.relpath(path, directory)
589
- return gm.match(path=path)
775
+ return GITMATCH_COMPILED.match(path=path)
590
776
 
591
777
 
592
778
  class WatchHandler(FileSystemEventHandler):
@@ -601,7 +787,9 @@ class WatchHandler(FileSystemEventHandler):
601
787
  if event.event_type == "created" or event.event_type == "modified":
602
788
  if not matches_ignore_patterns(path=event.src_path.replace("\\", "/"), directory=self.directory):
603
789
  new_checksum = calculate_md5(event.src_path)
604
- if event.src_path not in FILE_CHECKSUMS or (new_checksum and FILE_CHECKSUMS[event.src_path] != new_checksum):
790
+ if event.src_path not in FILE_CHECKSUMS or (
791
+ new_checksum and FILE_CHECKSUMS[event.src_path] != new_checksum
792
+ ):
605
793
  FILE_CHECKSUMS[event.src_path] = new_checksum
606
794
  LAST_MODIFIED["time"] = time.time()
607
795
  LAST_MODIFIED["files"][str(event.src_path)] = True
@@ -613,16 +801,31 @@ class WatchHandler(FileSystemEventHandler):
613
801
  # watch
614
802
  # =============================================================================
615
803
 
804
+
616
805
  @cli.command(context_settings=CONTEXT_SETTINGS)
617
806
  @common_params_for_installation
618
807
  @common_params_for_api
619
- @click.option("--restart", "-r", type=click.Choice(["yes", "no", "auto"]), default="auto", show_default=True, help="On package install: yes, force a restart | no, do not restart | auto, only restart if any .py files were changed")
620
- @click.option("--buffer", "-b", metavar="SECONDS", default=3, show_default=True, 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.")
808
+ @click.option(
809
+ "--restart",
810
+ "-r",
811
+ type=click.Choice(["yes", "no", "auto"]),
812
+ default="auto",
813
+ show_default=True,
814
+ help="On package install: yes, force a restart | no, do not restart | auto, only restart if any .py files were changed",
815
+ )
816
+ @click.option(
817
+ "--buffer",
818
+ "-b",
819
+ metavar="SECONDS",
820
+ default=3,
821
+ show_default=True,
822
+ 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.",
823
+ )
621
824
  def watch(directory, config, api, server, playground, restart, buffer):
622
825
  """
623
826
  Watch a package directory and `install` any changes. Press Ctrl + c to exit.
624
827
  """
625
- selected_server = select_server(*config, *api, server)
828
+ selected_server = select_server(*config, *api, server, watch=directory)
626
829
  restart_param = restart
627
830
  scan_directory(directory)
628
831
  global LAST_MODIFIED
@@ -632,10 +835,14 @@ def watch(directory, config, api, server, playground, restart, buffer):
632
835
  observer.start()
633
836
  click.echo()
634
837
  click.echo(f"""Server: {selected_server["name"]}""")
838
+
839
+ if "path" in selected_server and selected_server["path"] == directory:
840
+ playground = selected_server.get("playground", playground)
635
841
  if not playground:
636
842
  click.echo("Location: Package")
637
843
  else:
638
844
  click.echo(f"""Location: Playground "{playground}" """)
845
+
639
846
  click.echo(f"""Watching: {directory}""")
640
847
  click.secho(f"""[{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] Started""", fg="green")
641
848
  try:
@@ -652,20 +859,27 @@ def watch(directory, config, api, server, playground, restart, buffer):
652
859
  LAST_MODIFIED["time"] = 0
653
860
  LAST_MODIFIED["files"] = {}
654
861
  LAST_MODIFIED["restart"] = False
655
- package_installer(directory=directory, apiurl=selected_server["apiurl"], apikey=selected_server["apikey"], playground=playground, restart=restart)
862
+ package_installer(
863
+ directory=directory,
864
+ apiurl=selected_server["apiurl"],
865
+ apikey=selected_server["apikey"],
866
+ playground=playground,
867
+ restart=restart,
868
+ )
656
869
  time.sleep(1)
657
870
  except Exception as e:
658
871
  click.echo(f"\nException occurred: {e}")
659
872
  finally:
660
873
  observer.stop()
661
874
  observer.join()
662
- return("""\nStopping "docassemblecli3 watch".""")
875
+ return """\nStopping "docassemblecli3 watch"."""
663
876
 
664
877
 
665
878
  # =============================================================================
666
879
  # create
667
880
  # =============================================================================
668
881
 
882
+
669
883
  @cli.command(context_settings=CONTEXT_SETTINGS)
670
884
  @click.option("--package", metavar="PACKAGE", help="Name of the package you want to create")
671
885
  @click.option("--developer-name", metavar="NAME", help="Name of the developer of the package")
@@ -681,10 +895,10 @@ def create(package, developer_name, developer_email, description, url, license,
681
895
  """
682
896
  pkgname = package
683
897
  if not pkgname:
684
- pkgname = click.prompt("Name of the package you want to create (e.g., childsupport)")
898
+ pkgname = click.prompt("Name of the package you want to create (e.g., childsupport)")
685
899
  pkgname = re.sub(r"\s", "", pkgname)
686
900
  if not pkgname:
687
- return("The package name you entered is invalid.")
901
+ return "The package name you entered is invalid."
688
902
  pkgname = re.sub(r"^docassemble[\-\.]", "", pkgname, flags=re.IGNORECASE)
689
903
  if output:
690
904
  packagedir = output
@@ -692,10 +906,10 @@ def create(package, developer_name, developer_email, description, url, license,
692
906
  packagedir = "docassemble-" + pkgname
693
907
  if os.path.exists(packagedir):
694
908
  if not os.path.isdir(packagedir):
695
- return("Cannot create the directory " + packagedir + " because the path already exists.")
909
+ return "Cannot create the directory " + packagedir + " because the path already exists."
696
910
  dir_listing = list(os.listdir(packagedir))
697
911
  if "setup.py" in dir_listing or "setup.cfg" in dir_listing:
698
- return("The directory " + packagedir + " already has a package in it.")
912
+ return "The directory " + packagedir + " already has a package in it."
699
913
  else:
700
914
  os.makedirs(packagedir, exist_ok=True)
701
915
  if not developer_name:
@@ -724,7 +938,12 @@ __import__("pkg_resources").declare_namespace(__name__)
724
938
 
725
939
  """
726
940
  if "MIT" in license:
727
- licensetext = "The MIT License (MIT)\n\nCopyright (c) " + str(datetime.datetime.now().year) + " " + developer_name + """
941
+ licensetext = (
942
+ "The MIT License (MIT)\n\nCopyright (c) "
943
+ + str(datetime.datetime.now().year)
944
+ + " "
945
+ + developer_name
946
+ + """
728
947
 
729
948
  Permission is hereby granted, free of charge, to any person obtaining a copy
730
949
  of this software and associated documentation files (the "Software"), to deal
@@ -744,10 +963,21 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
744
963
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
745
964
  SOFTWARE.
746
965
  """
966
+ )
747
967
  else:
748
968
  licensetext = license + "\n"
749
969
 
750
- readme = "# docassemble." + pkgname + "\n\n" + description + "\n\n## Author\n\n" + developer_name + ", " + developer_email + "\n"
970
+ readme = (
971
+ "# docassemble."
972
+ + pkgname
973
+ + "\n\n"
974
+ + description
975
+ + "\n\n## Author\n\n"
976
+ + developer_name
977
+ + ", "
978
+ + developer_email
979
+ + "\n"
980
+ )
751
981
  manifestin = """\
752
982
  include README.md
753
983
  """
@@ -802,22 +1032,44 @@ def find_package_data(where=".", package="", exclude=standard_exclude, exclude_d
802
1032
  return out
803
1033
 
804
1034
  """
805
- setuppy += "setup(name=" + repr("docassemble." + pkgname) + """,
806
- version=""" + repr(version) + """,
807
- description=(""" + repr(description) + """),
808
- long_description=""" + repr(readme) + """,
1035
+ setuppy += (
1036
+ "setup(name="
1037
+ + repr("docassemble." + pkgname)
1038
+ + """,
1039
+ version="""
1040
+ + repr(version)
1041
+ + """,
1042
+ description=("""
1043
+ + repr(description)
1044
+ + """),
1045
+ long_description="""
1046
+ + repr(readme)
1047
+ + """,
809
1048
  long_description_content_type="text/markdown",
810
- author=""" + repr(developer_name) + """,
811
- author_email=""" + repr(developer_email) + """,
812
- license=""" + repr(license) + """,
813
- url=""" + repr(package_url) + """,
1049
+ author="""
1050
+ + repr(developer_name)
1051
+ + """,
1052
+ author_email="""
1053
+ + repr(developer_email)
1054
+ + """,
1055
+ license="""
1056
+ + repr(license)
1057
+ + """,
1058
+ url="""
1059
+ + repr(package_url)
1060
+ + """,
814
1061
  packages=find_packages(),
815
1062
  namespace_packages=["docassemble"],
816
1063
  install_requires=[],
817
1064
  zip_safe=False,
818
- package_data=find_package_data(where='docassemble/""" + pkgname + """/', package='docassemble.""" + pkgname + """'),
1065
+ package_data=find_package_data(where='docassemble/"""
1066
+ + pkgname
1067
+ + """/', package='docassemble."""
1068
+ + pkgname
1069
+ + """'),
819
1070
  )
820
1071
  """
1072
+ )
821
1073
  # maindir = os.path.join(packagedir, "docassemble", pkgname)
822
1074
  questionsdir = os.path.join(packagedir, "docassemble", pkgname, "data", "questions")
823
1075
  templatesdir = os.path.join(packagedir, "docassemble", pkgname, "data", "templates")
@@ -831,7 +1083,7 @@ def find_package_data(where=".", package="", exclude=standard_exclude, exclude_d
831
1083
  os.makedirs(staticdir, exist_ok=True)
832
1084
  if not os.path.isdir(sourcesdir):
833
1085
  os.makedirs(sourcesdir, exist_ok=True)
834
- with open(os.path.join(packagedir, '.gitignore'), 'w', encoding='utf-8') as the_file:
1086
+ with open(os.path.join(packagedir, ".gitignore"), "w", encoding="utf-8") as the_file:
835
1087
  the_file.write(GITIGNORE)
836
1088
  with open(os.path.join(packagedir, "README.md"), "w", encoding="utf-8") as the_file:
837
1089
  the_file.write(readme)
@@ -854,9 +1106,16 @@ def find_package_data(where=".", package="", exclude=standard_exclude, exclude_d
854
1106
  # config
855
1107
  # =============================================================================
856
1108
 
1109
+
857
1110
  @config.command(context_settings=CONTEXT_SETTINGS)
858
1111
  @common_params_for_config
859
- @click.option("--api", "-a", type=(APIURLType(), str), default=(None, None), help="URL of the docassemble server and API key of the user (admin or developer)")
1112
+ @click.option(
1113
+ "--api",
1114
+ "-a",
1115
+ type=(APIURLType(), str),
1116
+ default=(None, None),
1117
+ help="URL of the docassemble server and API key of the user (admin or developer)",
1118
+ )
860
1119
  def add(config, api):
861
1120
  """
862
1121
  Add a server to the config file.
@@ -924,7 +1183,9 @@ def new(config):
924
1183
  def server_version(config, api, server):
925
1184
  selected_server = select_server(*config, *api, server)
926
1185
  try:
927
- r = requests.get(selected_server["apiurl"] + "/api/package", headers={"X-API-Key": selected_server["apikey"]}, timeout=600)
1186
+ r = requests.get(
1187
+ selected_server["apiurl"] + "/api/package", headers={"X-API-Key": selected_server["apikey"]}, timeout=600
1188
+ )
928
1189
  if DEBUG:
929
1190
  click.echo(type(r.status_code))
930
1191
  click.echo(r.status_code)
@@ -953,4 +1214,3 @@ def test(config, api, server):
953
1214
  apikey = selected_server["apikey"]
954
1215
  click.echo(apiurl)
955
1216
  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.2.2"
7
+ version = "0.3.3"
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.8"
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