circup 2.0.4__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
circup/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  """
5
- CircUp -- a utility to manage and update libraries on a CircuitPython device.
5
+ Circup -- a utility to manage and update libraries on a CircuitPython device.
6
6
  """
7
7
 
8
8
 
circup/backends.py CHANGED
@@ -73,7 +73,7 @@ class Backend:
73
73
  """
74
74
  return self.get_modules(os.path.join(self.device_location, self.LIB_DIR_PATH))
75
75
 
76
- def _create_library_directory(self, device_path, library_path):
76
+ def create_directory(self, device_path, directory):
77
77
  """
78
78
  To be overridden by subclass
79
79
  """
@@ -97,6 +97,12 @@ class Backend:
97
97
  """
98
98
  raise NotImplementedError
99
99
 
100
+ def upload_file(self, target_file, location_to_paste):
101
+ """Paste a copy of the specified file at the location given
102
+ To be overridden by subclass
103
+ """
104
+ raise NotImplementedError
105
+
100
106
  # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks,too-many-statements
101
107
  def install_module(
102
108
  self, device_path, device_modules, name, pyext, mod_names, upgrade=False
@@ -189,7 +195,7 @@ class Backend:
189
195
  return
190
196
 
191
197
  # Create the library directory first.
192
- self._create_library_directory(device_path, library_path)
198
+ self.create_directory(device_path, library_path)
193
199
  if local_path is None:
194
200
  if pyext:
195
201
  # Use Python source for module.
@@ -281,7 +287,9 @@ class WebBackend(Backend):
281
287
  ):
282
288
  super().__init__(logger)
283
289
  if password is None:
284
- raise ValueError("--host needs --password")
290
+ raise ValueError(
291
+ "Must pass --password or set CIRCUP_WEBWORKFLOW_PASSWORD environment variable"
292
+ )
285
293
 
286
294
  # pylint: disable=no-member
287
295
  # verify hostname/address
@@ -306,6 +314,7 @@ class WebBackend(Backend):
306
314
  self.library_path = self.device_location + "/" + self.LIB_DIR_PATH
307
315
  self.timeout = timeout
308
316
  self.version_override = version_override
317
+ self.FS_URL = urljoin(self.device_location, self.FS_PATH)
309
318
 
310
319
  def __repr__(self):
311
320
  return f"<WebBackend @{self.device_location}>"
@@ -546,10 +555,9 @@ class WebBackend(Backend):
546
555
  metadata["path"] = sfm_url
547
556
  result[sfm[:idx]] = metadata
548
557
 
549
- def _create_library_directory(self, device_path, library_path):
550
- url = urlparse(device_path)
551
- auth = HTTPBasicAuth("", url.password)
552
- with self.session.put(library_path, auth=auth, timeout=self.timeout) as r:
558
+ def create_directory(self, device_path, directory):
559
+ auth = HTTPBasicAuth("", self.password)
560
+ with self.session.put(directory, auth=auth, timeout=self.timeout) as r:
553
561
  if r.status_code == 409:
554
562
  _writeable_error()
555
563
  r.raise_for_status()
@@ -560,11 +568,56 @@ class WebBackend(Backend):
560
568
  self.device_location,
561
569
  "/".join(("fs", location_to_paste, target_file, "")),
562
570
  )
563
- self._create_library_directory(self.device_location, create_directory_url)
571
+ self.create_directory(self.device_location, create_directory_url)
564
572
  self.install_dir_http(target_file)
565
573
  else:
566
574
  self.install_file_http(target_file)
567
575
 
576
+ def upload_file(self, target_file, location_to_paste):
577
+ """
578
+ copy a file from the host PC to the microcontroller
579
+ :param target_file: file on the host PC to copy
580
+ :param location_to_paste: Location on the microcontroller to paste it.
581
+ :return:
582
+ """
583
+ if os.path.isdir(target_file):
584
+ create_directory_url = urljoin(
585
+ self.device_location,
586
+ "/".join(("fs", location_to_paste, target_file, "")),
587
+ )
588
+ self.create_directory(self.device_location, create_directory_url)
589
+ self.install_dir_http(target_file, location_to_paste)
590
+ else:
591
+ self.install_file_http(target_file, location_to_paste)
592
+
593
+ def download_file(self, target_file, location_to_paste):
594
+ """
595
+ Download a file from the MCU device to the local host PC
596
+ :param target_file: The file on the MCU to download
597
+ :param location_to_paste: The location on the host PC to put the downloaded copy.
598
+ :return:
599
+ """
600
+ auth = HTTPBasicAuth("", self.password)
601
+ with self.session.get(
602
+ self.FS_URL + target_file, timeout=self.timeout, auth=auth
603
+ ) as r:
604
+ if r.status_code == 404:
605
+ click.secho(f"{target_file} was not found on the device", "red")
606
+
607
+ file_name = target_file.split("/")[-1]
608
+ if location_to_paste is None:
609
+ with open(file_name, "wb") as f:
610
+ f.write(r.content)
611
+
612
+ click.echo(f"Downloaded File: {file_name}")
613
+ else:
614
+ with open(os.path.join(location_to_paste, file_name), "wb") as f:
615
+ f.write(r.content)
616
+
617
+ click.echo(
618
+ f"Downloaded File: {os.path.join(location_to_paste, file_name)}"
619
+ )
620
+
568
621
  def install_module_mpy(self, bundle, metadata):
569
622
  """
570
623
  :param bundle library bundle.
@@ -668,11 +721,7 @@ class WebBackend(Backend):
668
721
  """
669
722
  retuns the full path on the device to a given file name.
670
723
  """
671
- return urljoin(
672
- urljoin(self.device_location, "fs/", allow_fragments=False),
673
- filename,
674
- allow_fragments=False,
675
- )
724
+ return "/".join((self.device_location, "fs", filename))
676
725
 
677
726
  def is_device_present(self):
678
727
  """
@@ -743,6 +792,19 @@ class WebBackend(Backend):
743
792
  return r.json()["free"] * r.json()["block_size"] # bytes
744
793
  sys.exit(1)
745
794
 
795
+ def list_dir(self, dirpath):
796
+ """
797
+ Returns the list of files located in the given dirpath.
798
+ """
799
+ auth = HTTPBasicAuth("", self.password)
800
+ with self.session.get(
801
+ urljoin(self.device_location, f"fs/{dirpath if dirpath else ''}"),
802
+ auth=auth,
803
+ headers={"Accept": "application/json"},
804
+ timeout=self.timeout,
805
+ ) as r:
806
+ return r.json()["files"]
807
+
746
808
 
747
809
  class DiskBackend(Backend):
748
810
  """
@@ -821,9 +883,9 @@ class DiskBackend(Backend):
821
883
  """
822
884
  return _get_modules_file(device_lib_path, self.logger)
823
885
 
824
- def _create_library_directory(self, device_path, library_path):
825
- if not os.path.exists(library_path): # pragma: no cover
826
- os.makedirs(library_path)
886
+ def create_directory(self, device_path, directory):
887
+ if not os.path.exists(directory): # pragma: no cover
888
+ os.makedirs(directory)
827
889
 
828
890
  def copy_file(self, target_file, location_to_paste):
829
891
  target_filename = target_file.split(os.path.sep)[-1]
@@ -838,6 +900,9 @@ class DiskBackend(Backend):
838
900
  os.path.join(self.device_location, location_to_paste, target_filename),
839
901
  )
840
902
 
903
+ def upload_file(self, target_file, location_to_paste):
904
+ self.copy_file(target_file, location_to_paste)
905
+
841
906
  def install_module_mpy(self, bundle, metadata):
842
907
  """
843
908
  :param bundle library bundle.
@@ -885,7 +950,10 @@ class DiskBackend(Backend):
885
950
  # Copy the directory.
886
951
  shutil.copytree(source_path, target_path)
887
952
  else:
888
- target = os.path.basename(source_path)
953
+ if "target_name" in metadata:
954
+ target = metadata["target_name"]
955
+ else:
956
+ target = os.path.basename(source_path)
889
957
  target_path = os.path.join(location, target)
890
958
  # Copy file.
891
959
  shutil.copyfile(source_path, target_path)
circup/command_utils.py CHANGED
@@ -100,6 +100,7 @@ def completion_for_example(ctx, param, incomplete):
100
100
  Returns the list of available modules for the command line tab-completion
101
101
  with the ``circup example`` command.
102
102
  """
103
+
103
104
  # pylint: disable=unused-argument, consider-iterating-dictionary
104
105
  available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
105
106
 
@@ -319,14 +320,22 @@ def get_bundle_examples(bundles_list, avoid_download=False):
319
320
  :return: A dictionary of metadata about the examples available in the
320
321
  library bundle.
321
322
  """
322
- # pylint: disable=too-many-nested-blocks
323
+ # pylint: disable=too-many-nested-blocks,too-many-locals
323
324
  all_the_examples = dict()
325
+ bundle_examples = dict()
324
326
 
325
327
  try:
326
328
  for bundle in bundles_list:
327
329
  if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
328
330
  ensure_latest_bundle(bundle)
329
331
  path = bundle.examples_dir("py")
332
+ meta_saved = os.path.join(path, "../bundle_examples.json")
333
+ if os.path.exists(meta_saved):
334
+ with open(meta_saved, "r", encoding="utf-8") as f:
335
+ bundle_examples = json.load(f)
336
+ all_the_examples.update(bundle_examples)
337
+ bundle_examples.clear()
338
+ continue
330
339
  path_examples = _get_modules_file(path, logger)
331
340
  for lib_name, lib_metadata in path_examples.items():
332
341
  for _dir_level in os.walk(lib_metadata["path"]):
@@ -337,8 +346,13 @@ def get_bundle_examples(bundles_list, avoid_download=False):
337
346
  if _dirs[-1] == "":
338
347
  _dirs.pop(-1)
339
348
  slug = f"{os.path.sep}".join(_dirs + [_file.replace(".py", "")])
349
+ bundle_examples[slug] = os.path.join(_dir_level[0], _file)
340
350
  all_the_examples[slug] = os.path.join(_dir_level[0], _file)
341
351
 
352
+ with open(meta_saved, "w", encoding="utf-8") as f:
353
+ json.dump(bundle_examples, f)
354
+ bundle_examples.clear()
355
+
342
356
  except NotADirectoryError:
343
357
  # Bundle does not have new style examples directory
344
358
  # so we cannot include its examples.
@@ -625,3 +639,29 @@ def get_device_path(host, port, password, path):
625
639
  else:
626
640
  device_path = find_device()
627
641
  return device_path
642
+
643
+
644
+ def sorted_by_directory_then_alpha(list_of_files):
645
+ """
646
+ Sort the list of files into alphabetical seperated
647
+ with directories grouped together before files.
648
+ """
649
+ dirs = {}
650
+ files = {}
651
+
652
+ for cur_file in list_of_files:
653
+ if cur_file["directory"]:
654
+ dirs[cur_file["name"]] = cur_file
655
+ else:
656
+ files[cur_file["name"]] = cur_file
657
+
658
+ sorted_dir_names = sorted(dirs.keys())
659
+ sorted_file_names = sorted(files.keys())
660
+
661
+ sorted_full_list = []
662
+ for cur_name in sorted_dir_names:
663
+ sorted_full_list.append(dirs[cur_name])
664
+ for cur_name in sorted_file_names:
665
+ sorted_full_list.append(files[cur_name])
666
+
667
+ return sorted_full_list
circup/commands.py CHANGED
@@ -84,7 +84,7 @@ from circup.command_utils import (
84
84
  "with --board-id, it overrides the detected CPy version.",
85
85
  )
86
86
  @click.version_option(
87
- prog_name="CircUp",
87
+ prog_name="Circup",
88
88
  message="%(prog)s, A CircuitPython module updater. Version %(version)s",
89
89
  )
90
90
  @click.pass_context
@@ -94,7 +94,7 @@ def main( # pylint: disable=too-many-locals
94
94
  """
95
95
  A tool to manage and update libraries on a CircuitPython device.
96
96
  """
97
- # pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals
97
+ # pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
98
98
  ctx.ensure_object(dict)
99
99
  ctx.obj["TIMEOUT"] = timeout
100
100
 
@@ -178,8 +178,8 @@ def main( # pylint: disable=too-many-locals
178
178
  else (cpy_version, board_id)
179
179
  )
180
180
  click.echo(
181
- "Found device at {}, running CircuitPython {}.".format(
182
- device_path, cpy_version
181
+ "Found device {} at {}, running CircuitPython {}.".format(
182
+ board_id, device_path, cpy_version
183
183
  )
184
184
  )
185
185
  try:
@@ -406,31 +406,53 @@ def install(
406
406
 
407
407
  @main.command()
408
408
  @click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.")
409
+ @click.option("--list", "-ls", "op_list", is_flag=True, help="List available examples.")
410
+ @click.option("--rename", is_flag=True, help="Install the example as code.py.")
409
411
  @click.argument(
410
- "examples", required=True, nargs=-1, shell_complete=completion_for_example
412
+ "examples", required=False, nargs=-1, shell_complete=completion_for_example
411
413
  )
412
414
  @click.pass_context
413
- def example(ctx, examples, overwrite):
415
+ def example(ctx, examples, op_list, rename, overwrite):
414
416
  """
415
417
  Copy named example(s) from a bundle onto the device. Multiple examples
416
418
  can be installed at once by providing more than one example name, each
417
419
  separated by a space.
418
420
  """
419
421
 
422
+ if op_list:
423
+ if examples:
424
+ click.echo("\n".join(completion_for_example(ctx, "", examples)))
425
+ else:
426
+ click.echo("Available example libraries:")
427
+ available_examples = get_bundle_examples(
428
+ get_bundles_list(), avoid_download=True
429
+ )
430
+ lib_names = {
431
+ str(key.split(os.path.sep)[0]): value
432
+ for key, value in available_examples.items()
433
+ }
434
+ click.echo("\n".join(sorted(lib_names.keys())))
435
+ return
436
+
420
437
  for example_arg in examples:
421
438
  available_examples = get_bundle_examples(
422
439
  get_bundles_list(), avoid_download=True
423
440
  )
424
441
  if example_arg in available_examples:
425
442
  filename = available_examples[example_arg].split(os.path.sep)[-1]
443
+ install_metadata = {"path": available_examples[example_arg]}
444
+
445
+ filename = available_examples[example_arg].split(os.path.sep)[-1]
446
+ if rename:
447
+ if os.path.isfile(available_examples[example_arg]):
448
+ filename = "code.py"
449
+ install_metadata["target_name"] = filename
426
450
 
427
451
  if overwrite or not ctx.obj["backend"].file_exists(filename):
428
452
  click.echo(
429
453
  f"{'Copying' if not overwrite else 'Overwriting'}: {filename}"
430
454
  )
431
- ctx.obj["backend"].install_module_py(
432
- {"path": available_examples[example_arg]}, location=""
433
- )
455
+ ctx.obj["backend"].install_module_py(install_metadata, location="")
434
456
  else:
435
457
  click.secho(
436
458
  f"File: {filename} already exists. Use --overwrite if you wish to replace it.",
circup/shared.py CHANGED
@@ -79,14 +79,18 @@ def _get_modules_file(path, logger):
79
79
  py_files = glob.glob(os.path.join(package_path, "**/*.py"), recursive=True)
80
80
  mpy_files = glob.glob(os.path.join(package_path, "**/*.mpy"), recursive=True)
81
81
  all_files = py_files + mpy_files
82
+ # put __init__ first if any, assumed to have the version number
83
+ all_files.sort()
82
84
  # default value
83
85
  result[name] = {"path": package_path, "mpy": bool(mpy_files)}
84
86
  # explore all the submodules to detect bad ones
85
87
  for source in [f for f in all_files if not os.path.basename(f).startswith(".")]:
86
88
  metadata = extract_metadata(source, logger)
87
89
  if "__version__" in metadata:
88
- metadata["path"] = package_path
89
- result[name] = metadata
90
+ # don't replace metadata if already found
91
+ if "__version__" not in result[name]:
92
+ metadata["path"] = package_path
93
+ result[name] = metadata
90
94
  # break now if any of the submodules has a bad format
91
95
  if metadata["__version__"] == BAD_FILE_FORMAT:
92
96
  break
@@ -0,0 +1,105 @@
1
+
2
+ wwshell
3
+ =======
4
+
5
+ .. image:: https://readthedocs.org/projects/circup/badge/?version=latest
6
+ :target: https://circuitpython.readthedocs.io/projects/circup/en/latest/
7
+ :alt: Documentation Status
8
+
9
+ .. image:: https://img.shields.io/discord/327254708534116352.svg
10
+ :target: https://adafru.it/discord
11
+ :alt: Discord
12
+
13
+
14
+ .. image:: https://github.com/adafruit/circup/workflows/Build%20CI/badge.svg
15
+ :target: https://github.com/adafruit/circup/actions
16
+ :alt: Build Status
17
+
18
+
19
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
20
+ :target: https://github.com/psf/black
21
+ :alt: Code Style: Black
22
+
23
+
24
+ A tool to manage files on a CircuitPython device via wireless workflows.
25
+ Currently supports Web Workflow.
26
+
27
+ .. contents::
28
+
29
+ Installation
30
+ ------------
31
+
32
+ wwshell is bundled along with Circup. When you install Circup you'll get wwshell automatically.
33
+
34
+ Circup requires Python 3.5 or higher.
35
+
36
+ In a `virtualenv <https://virtualenv.pypa.io/en/latest/>`_,
37
+ ``pip install circup`` should do the trick. This is the simplest way to make it
38
+ work.
39
+
40
+ If you have no idea what a virtualenv is, try the following command,
41
+ ``pip3 install --user circup``.
42
+
43
+ .. note::
44
+
45
+ If you use the ``pip3`` command to install CircUp you must make sure that
46
+ your path contains the directory into which the script will be installed.
47
+ To discover this path,
48
+
49
+ * On Unix-like systems, type ``python3 -m site --user-base`` and append
50
+ ``bin`` to the resulting path.
51
+ * On Windows, type the same command, but append ``Scripts`` to the
52
+ resulting path.
53
+
54
+ What does wwshell do?
55
+ ---------------------
56
+
57
+ It lets you view, delete, upload, and download files from your Circuitpython device
58
+ via wireless workflows. Similar to ampy, but operates over wireless workflow rather
59
+ than USB serial.
60
+
61
+ Usage
62
+ -----
63
+
64
+ To use web workflow you need to enable it by putting WIFI credentials and a web workflow
65
+ password into your settings.toml file. `See here <https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor/device-setup>`_,
66
+
67
+ To get help, just type the command::
68
+
69
+ $ wwshell
70
+ Usage: wwshell [OPTIONS] COMMAND [ARGS]...
71
+
72
+ A tool to manage files CircuitPython device over web workflow.
73
+
74
+ Options:
75
+ --verbose Comprehensive logging is sent to stdout.
76
+ --path DIRECTORY Path to CircuitPython directory. Overrides automatic path
77
+ detection.
78
+ --host TEXT Hostname or IP address of a device. Overrides automatic
79
+ path detection.
80
+ --password TEXT Password to use for authentication when --host is used.
81
+ You can optionally set an environment variable
82
+ CIRCUP_WEBWORKFLOW_PASSWORD instead of passing this
83
+ argument. If both exist the CLI arg takes precedent.
84
+ --timeout INTEGER Specify the timeout in seconds for any network
85
+ operations.
86
+ --version Show the version and exit.
87
+ --help Show this message and exit.
88
+
89
+ Commands:
90
+ get Download a copy of a file or directory from the device to the...
91
+ ls Lists the contents of a directory.
92
+ put Upload a copy of a file or directory from the local computer to...
93
+ rm Delete a file on the device.
94
+
95
+
96
+ .. note::
97
+
98
+ If you find a bug, or you want to suggest an enhancement or new feature
99
+ feel free to create an issue or submit a pull request here:
100
+
101
+ https://github.com/adafruit/circup
102
+
103
+
104
+ Discussion of this tool happens on the Adafruit CircuitPython
105
+ `Discord channel <https://discord.gg/rqrKDjU>`_.
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ wwshell is a CLI utility for managing files on CircuitPython devices via wireless workflows.
6
+ It currently supports Web Workflow.
7
+ """
8
+ from .commands import main
9
+
10
+
11
+ # Allows execution via `python -m circup ...`
12
+ # pylint: disable=no-value-for-parameter
13
+ if __name__ == "__main__":
14
+ main()
@@ -0,0 +1,231 @@
1
+ # SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ # ----------- CLI command definitions ----------- #
6
+
7
+ The following functions have IO side effects (for instance they emit to
8
+ stdout). Ergo, these are not checked with unit tests. Most of the
9
+ functionality they provide is provided by the functions from util_functions.py,
10
+ and the respective Backends which *are* tested. Most of the logic of the following
11
+ functions is to prepare things for presentation to / interaction with the user.
12
+ """
13
+ import os
14
+ import time
15
+ import sys
16
+ import logging
17
+ import update_checker
18
+ import click
19
+ import requests
20
+
21
+
22
+ from circup.backends import WebBackend
23
+ from circup.logging import logger, log_formatter, LOGFILE
24
+ from circup.shared import BOARDLESS_COMMANDS
25
+
26
+ from circup.command_utils import (
27
+ get_device_path,
28
+ get_circup_version,
29
+ sorted_by_directory_then_alpha,
30
+ )
31
+
32
+
33
+ @click.group()
34
+ @click.option(
35
+ "--verbose", is_flag=True, help="Comprehensive logging is sent to stdout."
36
+ )
37
+ @click.option(
38
+ "--path",
39
+ type=click.Path(exists=True, file_okay=False),
40
+ help="Path to CircuitPython directory. Overrides automatic path detection.",
41
+ )
42
+ @click.option(
43
+ "--host",
44
+ help="Hostname or IP address of a device. Overrides automatic path detection.",
45
+ default="circuitpython.local",
46
+ )
47
+ @click.option(
48
+ "--port",
49
+ help="HTTP port that the web workflow is listening on.",
50
+ default=80,
51
+ )
52
+ @click.option(
53
+ "--password",
54
+ help="Password to use for authentication when --host is used."
55
+ " You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
56
+ " instead of passing this argument. If both exist the CLI arg takes precedent.",
57
+ )
58
+ @click.option(
59
+ "--timeout",
60
+ default=30,
61
+ help="Specify the timeout in seconds for any network operations.",
62
+ )
63
+ @click.version_option(
64
+ prog_name="CircFile",
65
+ message="%(prog)s, A CircuitPython web workflow file managemenr. Version %(version)s",
66
+ )
67
+ @click.pass_context
68
+ def main( # pylint: disable=too-many-locals
69
+ ctx,
70
+ verbose,
71
+ path,
72
+ host,
73
+ port,
74
+ password,
75
+ timeout,
76
+ ): # pragma: no cover
77
+ """
78
+ A tool to manage files CircuitPython device over web workflow.
79
+ """
80
+ # pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
81
+ ctx.ensure_object(dict)
82
+ ctx.obj["TIMEOUT"] = timeout
83
+
84
+ if password is None:
85
+ password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
86
+
87
+ device_path = get_device_path(host, port, password, path)
88
+
89
+ using_webworkflow = "host" in ctx.params.keys() and ctx.params["host"] is not None
90
+ if using_webworkflow:
91
+ if host == "circuitpython.local":
92
+ click.echo("Checking versions.json on circuitpython.local to find hostname")
93
+ versions_resp = requests.get(
94
+ "http://circuitpython.local/cp/version.json", timeout=timeout
95
+ )
96
+ host = f'{versions_resp.json()["hostname"]}.local'
97
+ click.echo(f"Using hostname: {host}")
98
+ device_path = device_path.replace("circuitpython.local", host)
99
+ try:
100
+ ctx.obj["backend"] = WebBackend(
101
+ host=host, port=port, password=password, logger=logger, timeout=timeout
102
+ )
103
+ except ValueError as e:
104
+ click.secho(e, fg="red")
105
+ time.sleep(0.3)
106
+ sys.exit(1)
107
+ except RuntimeError as e:
108
+ click.secho(e, fg="red")
109
+ sys.exit(1)
110
+
111
+ if verbose:
112
+ # Configure additional logging to stdout.
113
+ ctx.obj["verbose"] = True
114
+ verbose_handler = logging.StreamHandler(sys.stdout)
115
+ verbose_handler.setLevel(logging.INFO)
116
+ verbose_handler.setFormatter(log_formatter)
117
+ logger.addHandler(verbose_handler)
118
+ click.echo("Logging to {}\n".format(LOGFILE))
119
+ else:
120
+ ctx.obj["verbose"] = False
121
+
122
+ logger.info("### Started Circfile ###")
123
+
124
+ # If a newer version of circfile is available, print a message.
125
+ logger.info("Checking for a newer version of circfile")
126
+ version = get_circup_version()
127
+ if version:
128
+ update_checker.update_check("circfile", version)
129
+
130
+ # stop early if the command is boardless
131
+ if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
132
+ return
133
+
134
+ ctx.obj["DEVICE_PATH"] = device_path
135
+
136
+ if device_path is None or not ctx.obj["backend"].is_device_present():
137
+ click.secho("Could not find a connected CircuitPython device.", fg="red")
138
+ sys.exit(1)
139
+ else:
140
+ click.echo("Found device at {}.".format(device_path))
141
+
142
+
143
+ @main.command("ls")
144
+ @click.argument("file", required=True, nargs=1, default="/")
145
+ @click.pass_context
146
+ def ls_cli(ctx, file): # pragma: no cover
147
+ """
148
+ Lists the contents of a directory. Defaults to root directory
149
+ if not supplied.
150
+ """
151
+ logger.info("ls")
152
+ if not file.endswith("/"):
153
+ file += "/"
154
+ click.echo(f"running: ls {file}")
155
+
156
+ files = ctx.obj["backend"].list_dir(file)
157
+ click.echo("Size\tName")
158
+ for cur_file in sorted_by_directory_then_alpha(files):
159
+ click.echo(
160
+ f"{cur_file['file_size']}\t{cur_file['name']}{'/' if cur_file['directory'] else ''}"
161
+ )
162
+
163
+
164
+ @main.command("put")
165
+ @click.argument("file", required=True, nargs=1)
166
+ @click.argument("location", required=False, nargs=1, default="")
167
+ @click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.")
168
+ @click.pass_context
169
+ def put_cli(ctx, file, location, overwrite):
170
+ """
171
+ Upload a copy of a file or directory from the local computer
172
+ to the device
173
+ """
174
+ click.echo(f"Attempting PUT: {file} at {location} overwrite? {overwrite}")
175
+ if not ctx.obj["backend"].file_exists(f"{location}{file}"):
176
+ ctx.obj["backend"].upload_file(file, location)
177
+ click.echo(f"Successfully PUT {location}{file}")
178
+ else:
179
+ if overwrite:
180
+ click.secho(
181
+ f"{location}{file} already exists. Overwriting it.", fg="yellow"
182
+ )
183
+ ctx.obj["backend"].upload_file(file, location)
184
+ click.echo(f"Successfully PUT {location}{file}")
185
+ else:
186
+ click.secho(
187
+ f"{location}{file} already exists. Pass --overwrite if you wish to replace it.",
188
+ fg="red",
189
+ )
190
+
191
+
192
+ # pylint: enable=too-many-arguments,too-many-locals
193
+
194
+
195
+ @main.command("get")
196
+ @click.argument("file", required=True, nargs=1)
197
+ @click.argument("location", required=False, nargs=1)
198
+ @click.pass_context
199
+ def get_cli(ctx, file, location): # pragma: no cover
200
+ """
201
+ Download a copy of a file or directory from the device to the local computer.
202
+ """
203
+
204
+ click.echo(f"running: get {file} {location}")
205
+ ctx.obj["backend"].download_file(file, location)
206
+
207
+
208
+ @main.command("rm")
209
+ @click.argument("file", nargs=1)
210
+ @click.pass_context
211
+ def rm_cli(ctx, file): # pragma: no cover
212
+ """
213
+ Delete a file on the device.
214
+ """
215
+ click.echo(f"running: rm {file}")
216
+ ctx.obj["backend"].uninstall(
217
+ ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(file)
218
+ )
219
+
220
+
221
+ @main.command("mkdir")
222
+ @click.argument("directory", nargs=1)
223
+ @click.pass_context
224
+ def mkdir_cli(ctx, directory): # pragma: no cover
225
+ """
226
+ Create
227
+ """
228
+ click.echo(f"running: mkdir {directory}")
229
+ ctx.obj["backend"].create_directory(
230
+ ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(directory)
231
+ )
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: circup
3
- Version: 2.0.4
3
+ Version: 2.1.1
4
4
  Summary: A tool to manage/update libraries on CircuitPython devices.
5
5
  Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
6
  License: MIT License
@@ -51,15 +51,15 @@ Requires-Dist: findimports
51
51
  Requires-Dist: requests
52
52
  Requires-Dist: semver
53
53
  Requires-Dist: toml
54
- Requires-Dist: update-checker
54
+ Requires-Dist: update_checker
55
55
  Provides-Extra: optional
56
- Requires-Dist: pytest ; extra == 'optional'
57
- Requires-Dist: pytest-cov ; extra == 'optional'
58
- Requires-Dist: pytest-faulthandler ; extra == 'optional'
59
- Requires-Dist: pytest-random-order ; extra == 'optional'
56
+ Requires-Dist: pytest; extra == "optional"
57
+ Requires-Dist: pytest-cov; extra == "optional"
58
+ Requires-Dist: pytest-faulthandler; extra == "optional"
59
+ Requires-Dist: pytest-random-order; extra == "optional"
60
60
 
61
61
 
62
- CircUp
62
+ Circup
63
63
  ======
64
64
 
65
65
  .. image:: https://readthedocs.org/projects/circup/badge/?version=latest
@@ -88,7 +88,7 @@ A tool to manage and update libraries (modules) on a CircuitPython device.
88
88
  Installation
89
89
  ------------
90
90
 
91
- Circup requires Python 3.5 or higher.
91
+ Circup requires Python 3.9 or higher.
92
92
 
93
93
  In a `virtualenv <https://virtualenv.pypa.io/en/latest/>`_,
94
94
  ``pip install circup`` should do the trick. This is the simplest way to make it
@@ -99,7 +99,7 @@ If you have no idea what a virtualenv is, try the following command,
99
99
 
100
100
  .. note::
101
101
 
102
- If you use the ``pip3`` command to install CircUp you must make sure that
102
+ If you use the ``pip3`` command to install Circup you must make sure that
103
103
  your path contains the directory into which the script will be installed.
104
104
  To discover this path,
105
105
 
@@ -136,7 +136,7 @@ Usage
136
136
  -----
137
137
 
138
138
  If you need more detailed help using Circup see the Learn Guide article
139
- `"Use CircUp to easily keep your CircuitPython libraries up to date" <https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup/>`_.
139
+ `"Use Circup to easily keep your CircuitPython libraries up to date" <https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup/>`_.
140
140
 
141
141
  First, plug in a device running CircuiPython. This should appear as a mounted
142
142
  storage device called ``CIRCUITPY``.
@@ -290,7 +290,7 @@ The ``--version`` flag will tell you the current version of the
290
290
  ``circup`` command itself::
291
291
 
292
292
  $ circup --version
293
- CircUp, A CircuitPython module updater. Version 0.0.1
293
+ Circup, A CircuitPython module updater. Version 0.0.1
294
294
 
295
295
 
296
296
  To use circup via the `Web Workflow <https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor>`_. on devices that support it. Use the ``--host`` and ``--password`` arguments before your circup command.::
@@ -323,6 +323,7 @@ For Bash, add this to ~/.bashrc::
323
323
 
324
324
  For Zsh, add this to ~/.zshrc::
325
325
 
326
+ autoload -U compinit; compinit
326
327
  eval "$(_CIRCUP_COMPLETE=zsh_source circup)"
327
328
 
328
329
  For Fish, add this to ~/.config/fish/completions/foo-bar.fish::
@@ -0,0 +1,20 @@
1
+ circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
2
+ circup/backends.py,sha256=vJM2IstM6ITY23ry0tZvk54NFel8Mkvc60MtAXvez0o,39024
3
+ circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
4
+ circup/command_utils.py,sha256=W5l9Llh4X8DkKihhcPK4CeKCKjbUbaX6N374atQKREU,23908
5
+ circup/commands.py,sha256=4O7WtdNKKsjnx180og9PHwkq2NbN9_bZuCwMAQov1bY,28534
6
+ circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
7
+ circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
8
+ circup/shared.py,sha256=OnJRRKdLvoV7gsTntPyAh_YC6ClnUH0BsNNVGdFPwS4,8942
9
+ circup/config/bundle_config.json,sha256=oOJ3Rv-e008IhrRWwqIC3pEtyDsWe5_a4PGqzJOCHhk,202
10
+ circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
11
+ circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,3617
12
+ circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
13
+ circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
14
+ circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
15
+ circup-2.1.1.dist-info/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
16
+ circup-2.1.1.dist-info/METADATA,sha256=lWXkaRBez2c51pu6QfRPre4qmR8Lvj29k94s6hdI8Nc,13622
17
+ circup-2.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
18
+ circup-2.1.1.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
19
+ circup-2.1.1.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
20
+ circup-2.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  circup = circup:main
3
+ wwshell = circup.wwshell:main
@@ -1,16 +0,0 @@
1
- circup/__init__.py,sha256=VZqTFEKpNsEVAV7QimHTQLG1itDhj2U7IZ-HXlJbZpU,662
2
- circup/backends.py,sha256=_1g--PbC3P_nSa9hHybvQE5EH9nnDSOEk155j_k31ks,36350
3
- circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
4
- circup/command_utils.py,sha256=fXwtW-Z_te4WErdiGr7mxOsV-ggowVO6Kh4n5j762gQ,22578
5
- circup/commands.py,sha256=W2YSNjN0VfQKb6ck1WdSKUqr1e1S6q2_XxKYRDhi5KY,27505
6
- circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
7
- circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
8
- circup/shared.py,sha256=duxH4y0WiwAQPW5L68KKClPPMxXR9NQkOUBzDQm9c9k,8725
9
- circup/config/bundle_config.json,sha256=oOJ3Rv-e008IhrRWwqIC3pEtyDsWe5_a4PGqzJOCHhk,202
10
- circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
11
- circup-2.0.4.dist-info/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
12
- circup-2.0.4.dist-info/METADATA,sha256=gK-59Keso4LYcpqZRxg4bnKUbZxyFQ9Syale_F_tVUc,13591
13
- circup-2.0.4.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
14
- circup-2.0.4.dist-info/entry_points.txt,sha256=ppjKryNpv506fx84V8oHrl4uf_mIYtaBYMC77jRmX2I,39
15
- circup-2.0.4.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
16
- circup-2.0.4.dist-info/RECORD,,