circup 1.7.0__tar.gz → 1.9.0__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.
- {circup-1.7.0/circup.egg-info → circup-1.9.0}/PKG-INFO +10 -7
- {circup-1.7.0 → circup-1.9.0}/README.rst +3 -0
- {circup-1.7.0 → circup-1.9.0}/circup/backends.py +136 -37
- {circup-1.7.0 → circup-1.9.0}/circup/bundle.py +14 -0
- {circup-1.7.0 → circup-1.9.0}/circup/command_utils.py +78 -13
- {circup-1.7.0 → circup-1.9.0}/circup/commands.py +54 -3
- {circup-1.7.0 → circup-1.9.0/circup.egg-info}/PKG-INFO +10 -7
- {circup-1.7.0 → circup-1.9.0}/tests/import_styles.py +1 -1
- {circup-1.7.0 → circup-1.9.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {circup-1.7.0 → circup-1.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {circup-1.7.0 → circup-1.9.0}/.github/workflows/build.yml +0 -0
- {circup-1.7.0 → circup-1.9.0}/.github/workflows/release.yml +0 -0
- {circup-1.7.0 → circup-1.9.0}/.gitignore +0 -0
- {circup-1.7.0 → circup-1.9.0}/.isort.cfg +0 -0
- {circup-1.7.0 → circup-1.9.0}/.pre-commit-config.yaml +0 -0
- {circup-1.7.0 → circup-1.9.0}/.pylintrc +0 -0
- {circup-1.7.0 → circup-1.9.0}/CODE_OF_CONDUCT.rst +0 -0
- {circup-1.7.0 → circup-1.9.0}/CODE_OF_CONDUCT.rst.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/CONTRIBUTING.rst +0 -0
- {circup-1.7.0 → circup-1.9.0}/CONTRIBUTING.rst.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/LICENSE +0 -0
- {circup-1.7.0 → circup-1.9.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/LICENSES/MIT.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/LICENSES/Unlicense.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/README.rst.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup/__init__.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup/config/bundle_config.json +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup/config/bundle_config.json.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup/logging.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup/module.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup/shared.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup.egg-info/SOURCES.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup.egg-info/dependency_links.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup.egg-info/entry_points.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/circup.egg-info/requires.txt +6 -6
- {circup-1.7.0 → circup-1.9.0}/circup.egg-info/top_level.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/_static/favicon.ico +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/_static/favicon.ico.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/conf.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/index.rst +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/index.rst.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/logo.png +0 -0
- {circup-1.7.0 → circup-1.9.0}/docs/logo.png.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/optional_requirements.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/optional_requirements.txt.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/readthedocs.yml +0 -0
- {circup-1.7.0 → circup-1.9.0}/requirements.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/requirements.txt.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/setup.cfg +0 -0
- {circup-1.7.0 → circup-1.9.0}/setup.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/__init__.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/bad_module/__init__.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/bad_module/my_module.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/bad_python.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/bundle.json +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/bundle.json.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/device.json +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/device.json.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/dir_module/__init__.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/dir_module/my_module.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/local_module.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/local_module_cp7.mpy +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/local_module_cp7.mpy.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mock_device/boot_out.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mock_device/boot_out.txt.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mount_exists.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mount_exists.txt.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mount_missing.txt +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/mount_missing.txt.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/remote_module.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config.json +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config.json.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config_local.json +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config_local.json.license +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_circup.py +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_module.mpy +0 -0
- {circup-1.7.0 → circup-1.9.0}/tests/test_module.mpy.license +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: circup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: A tool to manage/update libraries on CircuitPython devices.
|
|
5
5
|
Home-page: https://github.com/adafruit/circup
|
|
6
6
|
Author: Adafruit Industries
|
|
@@ -57,16 +57,16 @@ Requires-Dist: sphinx; extra == "dev"
|
|
|
57
57
|
Requires-Dist: wheel; extra == "dev"
|
|
58
58
|
Requires-Dist: twine; extra == "dev"
|
|
59
59
|
Provides-Extra: all
|
|
60
|
+
Requires-Dist: pytest; extra == "all"
|
|
61
|
+
Requires-Dist: sphinx; extra == "all"
|
|
60
62
|
Requires-Dist: wheel; extra == "all"
|
|
61
|
-
Requires-Dist: pytest-faulthandler; extra == "all"
|
|
62
63
|
Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
|
|
63
|
-
Requires-Dist:
|
|
64
|
-
Requires-Dist: pytest; extra == "all"
|
|
64
|
+
Requires-Dist: pytest-faulthandler; extra == "all"
|
|
65
65
|
Requires-Dist: coverage; extra == "all"
|
|
66
|
-
Requires-Dist:
|
|
67
|
-
Requires-Dist: pytest-cov; extra == "all"
|
|
66
|
+
Requires-Dist: pylint; extra == "all"
|
|
68
67
|
Requires-Dist: twine; extra == "all"
|
|
69
|
-
Requires-Dist:
|
|
68
|
+
Requires-Dist: pytest-cov; extra == "all"
|
|
69
|
+
Requires-Dist: black; extra == "all"
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
CircUp
|
|
@@ -165,6 +165,8 @@ To get help, just type the command::
|
|
|
165
165
|
--host TEXT Hostname or IP address of a device. Overrides automatic
|
|
166
166
|
path detection.
|
|
167
167
|
--password TEXT Password to use for authentication when --host is used.
|
|
168
|
+
--timeout INTEGER Specify the timeout in seconds for any network
|
|
169
|
+
operations.
|
|
168
170
|
--board-id TEXT Manual Board ID of the CircuitPython device. If provided
|
|
169
171
|
in combination with --cpy-version, it overrides the
|
|
170
172
|
detected board ID.
|
|
@@ -177,6 +179,7 @@ To get help, just type the command::
|
|
|
177
179
|
bundle-add Add bundles to the local bundles list, by "user/repo"...
|
|
178
180
|
bundle-remove Remove one or more bundles from the local bundles list.
|
|
179
181
|
bundle-show Show the list of bundles, default and local, with URL,...
|
|
182
|
+
example Copy named example(s) from a bundle onto the device.
|
|
180
183
|
freeze Output details of all the modules found on the connected...
|
|
181
184
|
install Install a named module(s) onto the device.
|
|
182
185
|
list Lists all out of date modules found on the connected...
|
|
@@ -95,6 +95,8 @@ To get help, just type the command::
|
|
|
95
95
|
--host TEXT Hostname or IP address of a device. Overrides automatic
|
|
96
96
|
path detection.
|
|
97
97
|
--password TEXT Password to use for authentication when --host is used.
|
|
98
|
+
--timeout INTEGER Specify the timeout in seconds for any network
|
|
99
|
+
operations.
|
|
98
100
|
--board-id TEXT Manual Board ID of the CircuitPython device. If provided
|
|
99
101
|
in combination with --cpy-version, it overrides the
|
|
100
102
|
detected board ID.
|
|
@@ -107,6 +109,7 @@ To get help, just type the command::
|
|
|
107
109
|
bundle-add Add bundles to the local bundles list, by "user/repo"...
|
|
108
110
|
bundle-remove Remove one or more bundles from the local bundles list.
|
|
109
111
|
bundle-show Show the list of bundles, default and local, with URL,...
|
|
112
|
+
example Copy named example(s) from a bundle onto the device.
|
|
110
113
|
freeze Output details of all the modules found on the connected...
|
|
111
114
|
install Install a named module(s) onto the device.
|
|
112
115
|
list Lists all out of date modules found on the connected...
|
|
@@ -16,7 +16,6 @@ import requests
|
|
|
16
16
|
from requests.adapters import HTTPAdapter
|
|
17
17
|
from requests.auth import HTTPBasicAuth
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
from circup.shared import DATA_DIR, BAD_FILE_FORMAT, extract_metadata, _get_modules_file
|
|
21
20
|
|
|
22
21
|
#: The location to store a local copy of code.py for use with --auto and
|
|
@@ -79,21 +78,27 @@ class Backend:
|
|
|
79
78
|
"""
|
|
80
79
|
raise NotImplementedError
|
|
81
80
|
|
|
82
|
-
def
|
|
81
|
+
def install_module_py(self, metadata, location=None):
|
|
83
82
|
"""
|
|
84
83
|
To be overridden by subclass
|
|
85
84
|
"""
|
|
86
85
|
raise NotImplementedError
|
|
87
86
|
|
|
88
|
-
def
|
|
87
|
+
def install_module_mpy(self, bundle, metadata):
|
|
88
|
+
"""
|
|
89
|
+
To be overridden by subclass
|
|
89
90
|
"""
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
def copy_file(self, target_file, location_to_paste):
|
|
94
|
+
"""Paste a copy of the specified file at the location given
|
|
90
95
|
To be overridden by subclass
|
|
91
96
|
"""
|
|
92
97
|
raise NotImplementedError
|
|
93
98
|
|
|
94
|
-
# pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks
|
|
99
|
+
# pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks,too-many-statements
|
|
95
100
|
def install_module(
|
|
96
|
-
self, device_path, device_modules, name, pyext, mod_names
|
|
101
|
+
self, device_path, device_modules, name, pyext, mod_names, upgrade=False
|
|
97
102
|
): # pragma: no cover
|
|
98
103
|
"""
|
|
99
104
|
Finds a connected device and installs a given module name if it
|
|
@@ -108,23 +113,51 @@ class Backend:
|
|
|
108
113
|
source or from a pre-compiled module
|
|
109
114
|
:param mod_names: Dictionary of metadata from modules that can be generated
|
|
110
115
|
with get_bundle_versions()
|
|
116
|
+
:param bool upgrade: Upgrade the specified modules if they're already installed.
|
|
111
117
|
"""
|
|
118
|
+
local_path = None
|
|
119
|
+
if os.path.exists(name):
|
|
120
|
+
# local file exists use that.
|
|
121
|
+
local_path = name
|
|
122
|
+
name = local_path.split(os.path.sep)[-1]
|
|
123
|
+
name = name.replace(".py", "").replace(".mpy", "")
|
|
124
|
+
click.echo(f"Installing from local path: {local_path}")
|
|
125
|
+
|
|
112
126
|
if not name:
|
|
113
127
|
click.echo("No module name(s) provided.")
|
|
114
|
-
|
|
128
|
+
return
|
|
129
|
+
if name in mod_names or local_path is not None:
|
|
130
|
+
|
|
115
131
|
# Grab device modules to check if module already installed
|
|
116
132
|
if name in device_modules:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
if not upgrade:
|
|
134
|
+
# skip already installed modules if no -upgrade flag
|
|
135
|
+
click.echo("'{}' is already installed.".format(name))
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# uninstall the module before installing
|
|
139
|
+
name = name.lower()
|
|
140
|
+
_mod_names = {}
|
|
141
|
+
for module_item, _metadata in device_modules.items():
|
|
142
|
+
_mod_names[module_item.replace(".py", "").lower()] = _metadata
|
|
143
|
+
if name in _mod_names:
|
|
144
|
+
_metadata = _mod_names[name]
|
|
145
|
+
module_path = _metadata["path"]
|
|
146
|
+
self.uninstall(device_path, module_path)
|
|
147
|
+
|
|
148
|
+
new_module_size = 0
|
|
120
149
|
library_path = (
|
|
121
150
|
os.path.join(device_path, self.LIB_DIR_PATH)
|
|
122
151
|
if not isinstance(self, WebBackend)
|
|
123
152
|
else urljoin(device_path, self.LIB_DIR_PATH)
|
|
124
153
|
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
154
|
+
if local_path is None:
|
|
155
|
+
metadata = mod_names[name]
|
|
156
|
+
bundle = metadata["bundle"]
|
|
157
|
+
else:
|
|
158
|
+
metadata = {"path": local_path}
|
|
159
|
+
|
|
160
|
+
new_module_size = os.path.getsize(metadata["path"])
|
|
128
161
|
if os.path.isdir(metadata["path"]):
|
|
129
162
|
# pylint: disable=unused-variable
|
|
130
163
|
for dirpath, dirnames, filenames in os.walk(metadata["path"]):
|
|
@@ -132,7 +165,7 @@ class Backend:
|
|
|
132
165
|
fp = os.path.join(dirpath, f)
|
|
133
166
|
try:
|
|
134
167
|
if not os.path.islink(fp): # Ignore symbolic links
|
|
135
|
-
|
|
168
|
+
new_module_size += os.path.getsize(fp)
|
|
136
169
|
else:
|
|
137
170
|
self.logger.warning(
|
|
138
171
|
f"Skipping symbolic link in space calculation: {fp}"
|
|
@@ -142,27 +175,29 @@ class Backend:
|
|
|
142
175
|
f"Error: {e} - Skipping file in space calculation: {fp}"
|
|
143
176
|
)
|
|
144
177
|
|
|
145
|
-
if self.get_free_space() <
|
|
178
|
+
if self.get_free_space() < new_module_size:
|
|
146
179
|
self.logger.error(
|
|
147
180
|
f"Aborted installing module {name} - "
|
|
148
|
-
f"not enough free space ({
|
|
181
|
+
f"not enough free space ({new_module_size} < {self.get_free_space()})"
|
|
149
182
|
)
|
|
150
183
|
click.secho(
|
|
151
184
|
f"Aborted installing module {name} - "
|
|
152
|
-
f"not enough free space ({
|
|
185
|
+
f"not enough free space ({new_module_size} < {self.get_free_space()})",
|
|
153
186
|
fg="red",
|
|
154
187
|
)
|
|
155
188
|
return
|
|
156
189
|
|
|
157
190
|
# Create the library directory first.
|
|
158
191
|
self._create_library_directory(device_path, library_path)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
if local_path is None:
|
|
193
|
+
if pyext:
|
|
194
|
+
# Use Python source for module.
|
|
195
|
+
self.install_module_py(metadata)
|
|
196
|
+
else:
|
|
197
|
+
# Use pre-compiled mpy modules.
|
|
198
|
+
self.install_module_mpy(bundle, metadata)
|
|
163
199
|
else:
|
|
164
|
-
|
|
165
|
-
self._install_module_mpy(bundle, metadata)
|
|
200
|
+
self.copy_file(metadata["path"], "lib")
|
|
166
201
|
click.echo("Installed '{}'.".format(name))
|
|
167
202
|
else:
|
|
168
203
|
click.echo("Unknown module named, '{}'.".format(name))
|
|
@@ -219,6 +254,12 @@ class Backend:
|
|
|
219
254
|
board_id = ""
|
|
220
255
|
return circuit_python, board_id
|
|
221
256
|
|
|
257
|
+
def file_exists(self, filepath):
|
|
258
|
+
"""
|
|
259
|
+
To be overriden by subclass
|
|
260
|
+
"""
|
|
261
|
+
raise NotImplementedError
|
|
262
|
+
|
|
222
263
|
|
|
223
264
|
def _writeable_error():
|
|
224
265
|
click.secho(
|
|
@@ -250,7 +291,8 @@ class WebBackend(Backend):
|
|
|
250
291
|
else "Could not find or connect to specified device"
|
|
251
292
|
) from exc
|
|
252
293
|
|
|
253
|
-
self.
|
|
294
|
+
self.FS_PATH = "fs/"
|
|
295
|
+
self.LIB_DIR_PATH = f"{self.FS_PATH}lib/"
|
|
254
296
|
self.host = host
|
|
255
297
|
self.password = password
|
|
256
298
|
self.device_location = f"http://:{self.password}@{self.host}"
|
|
@@ -260,14 +302,20 @@ class WebBackend(Backend):
|
|
|
260
302
|
self.library_path = self.device_location + "/" + self.LIB_DIR_PATH
|
|
261
303
|
self.timeout = timeout
|
|
262
304
|
|
|
263
|
-
def install_file_http(self, source):
|
|
305
|
+
def install_file_http(self, source, location=None):
|
|
264
306
|
"""
|
|
265
307
|
Install file to device using web workflow.
|
|
266
308
|
:param source source file.
|
|
309
|
+
:param location the location on the device to copy the source
|
|
310
|
+
directory in to. If omitted is CIRCUITPY/lib/ used.
|
|
267
311
|
"""
|
|
268
312
|
file_name = source.split(os.path.sep)
|
|
269
313
|
file_name = file_name[-2] if file_name[-1] == "" else file_name[-1]
|
|
270
|
-
|
|
314
|
+
|
|
315
|
+
if location is None:
|
|
316
|
+
target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
|
|
317
|
+
else:
|
|
318
|
+
target = self.device_location + "/" + self.FS_PATH + location + file_name
|
|
271
319
|
|
|
272
320
|
auth = HTTPBasicAuth("", self.password)
|
|
273
321
|
|
|
@@ -277,14 +325,19 @@ class WebBackend(Backend):
|
|
|
277
325
|
_writeable_error()
|
|
278
326
|
r.raise_for_status()
|
|
279
327
|
|
|
280
|
-
def install_dir_http(self, source):
|
|
328
|
+
def install_dir_http(self, source, location=None):
|
|
281
329
|
"""
|
|
282
330
|
Install directory to device using web workflow.
|
|
283
331
|
:param source source directory.
|
|
332
|
+
:param location the location on the device to copy the source
|
|
333
|
+
directory in to. If omitted is CIRCUITPY/lib/ used.
|
|
284
334
|
"""
|
|
285
335
|
mod_name = source.split(os.path.sep)
|
|
286
336
|
mod_name = mod_name[-2] if mod_name[-1] == "" else mod_name[-1]
|
|
287
|
-
|
|
337
|
+
if location is None:
|
|
338
|
+
target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
|
|
339
|
+
else:
|
|
340
|
+
target = self.device_location + "/" + self.FS_PATH + location + mod_name
|
|
288
341
|
target = target + "/" if target[:-1] != "/" else target
|
|
289
342
|
url = urlparse(target)
|
|
290
343
|
auth = HTTPBasicAuth("", url.password)
|
|
@@ -490,7 +543,18 @@ class WebBackend(Backend):
|
|
|
490
543
|
_writeable_error()
|
|
491
544
|
r.raise_for_status()
|
|
492
545
|
|
|
493
|
-
def
|
|
546
|
+
def copy_file(self, target_file, location_to_paste):
|
|
547
|
+
if os.path.isdir(target_file):
|
|
548
|
+
create_directory_url = urljoin(
|
|
549
|
+
self.device_location,
|
|
550
|
+
"/".join(("fs", location_to_paste, target_file, "")),
|
|
551
|
+
)
|
|
552
|
+
self._create_library_directory(self.device_location, create_directory_url)
|
|
553
|
+
self.install_dir_http(target_file)
|
|
554
|
+
else:
|
|
555
|
+
self.install_file_http(target_file)
|
|
556
|
+
|
|
557
|
+
def install_module_mpy(self, bundle, metadata):
|
|
494
558
|
"""
|
|
495
559
|
:param bundle library bundle.
|
|
496
560
|
:param library_path library path
|
|
@@ -514,7 +578,7 @@ class WebBackend(Backend):
|
|
|
514
578
|
raise IOError("Cannot find compiled version of module.")
|
|
515
579
|
|
|
516
580
|
# pylint: enable=too-many-locals,too-many-branches
|
|
517
|
-
def
|
|
581
|
+
def install_module_py(self, metadata, location=None):
|
|
518
582
|
"""
|
|
519
583
|
:param library_path library path
|
|
520
584
|
:param metadata dictionary.
|
|
@@ -522,10 +586,10 @@ class WebBackend(Backend):
|
|
|
522
586
|
|
|
523
587
|
source_path = metadata["path"] # Path to Python source version.
|
|
524
588
|
if os.path.isdir(source_path):
|
|
525
|
-
self.install_dir_http(source_path)
|
|
589
|
+
self.install_dir_http(source_path, location=location)
|
|
526
590
|
|
|
527
591
|
else:
|
|
528
|
-
self.install_file_http(source_path)
|
|
592
|
+
self.install_file_http(source_path, location=location)
|
|
529
593
|
|
|
530
594
|
def get_auto_file_path(self, auto_file_path):
|
|
531
595
|
"""
|
|
@@ -560,6 +624,18 @@ class WebBackend(Backend):
|
|
|
560
624
|
"""
|
|
561
625
|
self._update_http(module)
|
|
562
626
|
|
|
627
|
+
def file_exists(self, filepath):
|
|
628
|
+
"""
|
|
629
|
+
return True if the file exists, otherwise False.
|
|
630
|
+
"""
|
|
631
|
+
auth = HTTPBasicAuth("", self.password)
|
|
632
|
+
resp = requests.get(
|
|
633
|
+
self.get_file_path(filepath), auth=auth, timeout=self.timeout
|
|
634
|
+
)
|
|
635
|
+
if resp.status_code == 200:
|
|
636
|
+
return True
|
|
637
|
+
return False
|
|
638
|
+
|
|
563
639
|
def _update_http(self, module):
|
|
564
640
|
"""
|
|
565
641
|
Update the module using web workflow.
|
|
@@ -733,10 +809,22 @@ class DiskBackend(Backend):
|
|
|
733
809
|
if not os.path.exists(library_path): # pragma: no cover
|
|
734
810
|
os.makedirs(library_path)
|
|
735
811
|
|
|
736
|
-
def
|
|
812
|
+
def copy_file(self, target_file, location_to_paste):
|
|
813
|
+
target_filename = target_file.split(os.path.sep)[-1]
|
|
814
|
+
if os.path.isdir(target_file):
|
|
815
|
+
shutil.copytree(
|
|
816
|
+
target_file,
|
|
817
|
+
os.path.join(self.device_location, location_to_paste, target_filename),
|
|
818
|
+
)
|
|
819
|
+
else:
|
|
820
|
+
shutil.copyfile(
|
|
821
|
+
target_file,
|
|
822
|
+
os.path.join(self.device_location, location_to_paste, target_filename),
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
def install_module_mpy(self, bundle, metadata):
|
|
737
826
|
"""
|
|
738
827
|
:param bundle library bundle.
|
|
739
|
-
:param library_path library path
|
|
740
828
|
:param metadata dictionary.
|
|
741
829
|
"""
|
|
742
830
|
module_name = os.path.basename(metadata["path"]).replace(".py", ".mpy")
|
|
@@ -763,21 +851,26 @@ class DiskBackend(Backend):
|
|
|
763
851
|
raise IOError("Cannot find compiled version of module.")
|
|
764
852
|
|
|
765
853
|
# pylint: enable=too-many-locals,too-many-branches
|
|
766
|
-
def
|
|
854
|
+
def install_module_py(self, metadata, location=None):
|
|
767
855
|
"""
|
|
768
|
-
:param library_path library path
|
|
769
856
|
:param metadata dictionary.
|
|
857
|
+
:param location the location on the device to copy the py module to.
|
|
858
|
+
If omitted is CIRCUITPY/lib/ used.
|
|
770
859
|
"""
|
|
860
|
+
if location is None:
|
|
861
|
+
location = self.library_path
|
|
862
|
+
else:
|
|
863
|
+
location = os.path.join(self.device_location, location)
|
|
771
864
|
|
|
772
865
|
source_path = metadata["path"] # Path to Python source version.
|
|
773
866
|
if os.path.isdir(source_path):
|
|
774
867
|
target = os.path.basename(os.path.dirname(source_path))
|
|
775
|
-
target_path = os.path.join(
|
|
868
|
+
target_path = os.path.join(location, target)
|
|
776
869
|
# Copy the directory.
|
|
777
870
|
shutil.copytree(source_path, target_path)
|
|
778
871
|
else:
|
|
779
872
|
target = os.path.basename(source_path)
|
|
780
|
-
target_path = os.path.join(
|
|
873
|
+
target_path = os.path.join(location, target)
|
|
781
874
|
# Copy file.
|
|
782
875
|
shutil.copyfile(source_path, target_path)
|
|
783
876
|
|
|
@@ -825,6 +918,12 @@ class DiskBackend(Backend):
|
|
|
825
918
|
os.remove(module.path)
|
|
826
919
|
shutil.copyfile(module.bundle_path, module.path)
|
|
827
920
|
|
|
921
|
+
def file_exists(self, filepath):
|
|
922
|
+
"""
|
|
923
|
+
return True if the file exists, otherwise False.
|
|
924
|
+
"""
|
|
925
|
+
return os.path.exists(os.path.join(self.device_location, filepath))
|
|
926
|
+
|
|
828
927
|
def get_file_path(self, filename):
|
|
829
928
|
"""
|
|
830
929
|
returns the full path on the device to a given file name.
|
|
@@ -61,6 +61,20 @@ class Bundle:
|
|
|
61
61
|
"lib",
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
+
def examples_dir(self, platform):
|
|
65
|
+
"""
|
|
66
|
+
This bundle's examples directory for the platform.
|
|
67
|
+
|
|
68
|
+
:param str platform: The platform identifier (py/6mpy/...).
|
|
69
|
+
:return: The path to the examples directory for the platform.
|
|
70
|
+
"""
|
|
71
|
+
tag = self.current_tag
|
|
72
|
+
return os.path.join(
|
|
73
|
+
self.dir.format(platform=platform),
|
|
74
|
+
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
75
|
+
"examples",
|
|
76
|
+
)
|
|
77
|
+
|
|
64
78
|
def requirements_for(self, library_name, toml_file=False):
|
|
65
79
|
"""
|
|
66
80
|
The requirements file for this library.
|
|
@@ -6,6 +6,7 @@ Functions called from commands in order to provide behaviors and return informat
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import ctypes
|
|
9
|
+
import glob
|
|
9
10
|
import os
|
|
10
11
|
|
|
11
12
|
from subprocess import check_output
|
|
@@ -90,9 +91,27 @@ def completion_for_install(ctx, param, incomplete):
|
|
|
90
91
|
module_names = {m.replace(".py", "") for m in available_modules}
|
|
91
92
|
if incomplete:
|
|
92
93
|
module_names = [name for name in module_names if name.startswith(incomplete)]
|
|
94
|
+
module_names.extend(glob.glob(f"{incomplete}*"))
|
|
93
95
|
return sorted(module_names)
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
def completion_for_example(ctx, param, incomplete):
|
|
99
|
+
"""
|
|
100
|
+
Returns the list of available modules for the command line tab-completion
|
|
101
|
+
with the ``circup example`` command.
|
|
102
|
+
"""
|
|
103
|
+
# pylint: disable=unused-argument, consider-iterating-dictionary
|
|
104
|
+
available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
|
|
105
|
+
|
|
106
|
+
matching_examples = [
|
|
107
|
+
example_path
|
|
108
|
+
for example_path in available_examples.keys()
|
|
109
|
+
if example_path.startswith(incomplete)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
return sorted(matching_examples)
|
|
113
|
+
|
|
114
|
+
|
|
96
115
|
def ensure_latest_bundle(bundle):
|
|
97
116
|
"""
|
|
98
117
|
Ensure that there's a copy of the latest library bundle available so circup
|
|
@@ -290,6 +309,43 @@ def get_bundle(bundle, tag):
|
|
|
290
309
|
click.echo("\nOK\n")
|
|
291
310
|
|
|
292
311
|
|
|
312
|
+
def get_bundle_examples(bundles_list, avoid_download=False):
|
|
313
|
+
"""
|
|
314
|
+
Return a dictionary of metadata from examples in the all of the bundles
|
|
315
|
+
specified by bundles_list argument.
|
|
316
|
+
|
|
317
|
+
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
|
|
318
|
+
:param bool avoid_download: if True, download the bundle only if missing.
|
|
319
|
+
:return: A dictionary of metadata about the examples available in the
|
|
320
|
+
library bundle.
|
|
321
|
+
"""
|
|
322
|
+
# pylint: disable=too-many-nested-blocks
|
|
323
|
+
all_the_examples = dict()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
for bundle in bundles_list:
|
|
327
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
|
|
328
|
+
ensure_latest_bundle(bundle)
|
|
329
|
+
path = bundle.examples_dir("py")
|
|
330
|
+
path_examples = _get_modules_file(path, logger)
|
|
331
|
+
for lib_name, lib_metadata in path_examples.items():
|
|
332
|
+
for _dir_level in os.walk(lib_metadata["path"]):
|
|
333
|
+
for _file in _dir_level[2]:
|
|
334
|
+
_parts = _dir_level[0].split(os.path.sep)
|
|
335
|
+
_lib_name_index = _parts.index(lib_name)
|
|
336
|
+
_dirs = _parts[_lib_name_index:]
|
|
337
|
+
if _dirs[-1] == "":
|
|
338
|
+
_dirs.pop(-1)
|
|
339
|
+
slug = f"{os.path.sep}".join(_dirs + [_file.replace(".py", "")])
|
|
340
|
+
all_the_examples[slug] = os.path.join(_dir_level[0], _file)
|
|
341
|
+
|
|
342
|
+
except NotADirectoryError:
|
|
343
|
+
# Bundle does not have new style examples directory
|
|
344
|
+
# so we cannot include its examples.
|
|
345
|
+
pass
|
|
346
|
+
return all_the_examples
|
|
347
|
+
|
|
348
|
+
|
|
293
349
|
def get_bundle_versions(bundles_list, avoid_download=False):
|
|
294
350
|
"""
|
|
295
351
|
Returns a dictionary of metadata from modules in the latest known release
|
|
@@ -392,6 +448,7 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()):
|
|
|
392
448
|
:param list(str) to_install: Modules already selected for installation.
|
|
393
449
|
:return: tuple of module names to install which we build
|
|
394
450
|
"""
|
|
451
|
+
# pylint: disable=too-many-branches
|
|
395
452
|
# Internal variables
|
|
396
453
|
_to_install = to_install
|
|
397
454
|
_requested_libraries = []
|
|
@@ -417,10 +474,14 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()):
|
|
|
417
474
|
_requested_libraries.append(canonical_lib_name)
|
|
418
475
|
except KeyError:
|
|
419
476
|
if canonical_lib_name not in WARNING_IGNORE_MODULES:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
477
|
+
if os.path.exists(canonical_lib_name):
|
|
478
|
+
_requested_libraries.append(canonical_lib_name)
|
|
479
|
+
else:
|
|
480
|
+
click.secho(
|
|
481
|
+
f"WARNING:\n\t{canonical_lib_name} "
|
|
482
|
+
f"is not a known CircuitPython library.",
|
|
483
|
+
fg="yellow",
|
|
484
|
+
)
|
|
424
485
|
|
|
425
486
|
if not _requested_libraries:
|
|
426
487
|
# If nothing is requested, we're done
|
|
@@ -430,16 +491,20 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()):
|
|
|
430
491
|
if library not in _to_install:
|
|
431
492
|
_to_install = _to_install + (library,)
|
|
432
493
|
# get the requirements.txt from bundle
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
494
|
+
try:
|
|
495
|
+
bundle = mod_names[library]["bundle"]
|
|
496
|
+
requirements_txt = bundle.requirements_for(library)
|
|
497
|
+
if requirements_txt:
|
|
498
|
+
_requested_libraries.extend(
|
|
499
|
+
libraries_from_requirements(requirements_txt)
|
|
500
|
+
)
|
|
439
501
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
502
|
+
circup_dependencies = get_circup_dependencies(bundle, library)
|
|
503
|
+
for circup_dependency in circup_dependencies:
|
|
504
|
+
_requested_libraries.append(circup_dependency)
|
|
505
|
+
except KeyError:
|
|
506
|
+
# don't check local file for further dependencies
|
|
507
|
+
pass
|
|
443
508
|
|
|
444
509
|
# we've processed this library, remove it from the list
|
|
445
510
|
_requested_libraries.remove(library)
|
|
@@ -38,6 +38,8 @@ from circup.command_utils import (
|
|
|
38
38
|
get_bundles_local_dict,
|
|
39
39
|
save_local_bundles,
|
|
40
40
|
get_bundles_dict,
|
|
41
|
+
completion_for_example,
|
|
42
|
+
get_bundle_examples,
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
|
|
@@ -284,6 +286,9 @@ def list_cli(ctx): # pragma: no cover
|
|
|
284
286
|
@click.option(
|
|
285
287
|
"--auto", "-a", is_flag=True, help="Install the modules imported in code.py."
|
|
286
288
|
)
|
|
289
|
+
@click.option(
|
|
290
|
+
"--upgrade", "-U", is_flag=True, help="Upgrade modules that are already installed."
|
|
291
|
+
)
|
|
287
292
|
@click.option(
|
|
288
293
|
"--auto-file",
|
|
289
294
|
default=None,
|
|
@@ -291,11 +296,13 @@ def list_cli(ctx): # pragma: no cover
|
|
|
291
296
|
" Also accepts an absolute path or a local ./ path.",
|
|
292
297
|
)
|
|
293
298
|
@click.pass_context
|
|
294
|
-
def install(
|
|
299
|
+
def install(
|
|
300
|
+
ctx, modules, pyext, requirement, auto, auto_file, upgrade=False
|
|
301
|
+
): # pragma: no cover
|
|
295
302
|
"""
|
|
296
303
|
Install a named module(s) onto the device. Multiple modules
|
|
297
304
|
can be installed at once by providing more than one module name, each
|
|
298
|
-
separated by a space.
|
|
305
|
+
separated by a space. Modules can be from a Bundle or local filepaths.
|
|
299
306
|
"""
|
|
300
307
|
|
|
301
308
|
# TODO: Ensure there's enough space on the device
|
|
@@ -343,7 +350,51 @@ def install(ctx, modules, pyext, requirement, auto, auto_file): # pragma: no co
|
|
|
343
350
|
click.echo(f"Ready to install: {to_install}\n")
|
|
344
351
|
for library in to_install:
|
|
345
352
|
ctx.obj["backend"].install_module(
|
|
346
|
-
ctx.obj["DEVICE_PATH"],
|
|
353
|
+
ctx.obj["DEVICE_PATH"],
|
|
354
|
+
device_modules,
|
|
355
|
+
library,
|
|
356
|
+
pyext,
|
|
357
|
+
mod_names,
|
|
358
|
+
upgrade,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@main.command()
|
|
363
|
+
@click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.")
|
|
364
|
+
@click.argument(
|
|
365
|
+
"examples", required=True, nargs=-1, shell_complete=completion_for_example
|
|
366
|
+
)
|
|
367
|
+
@click.pass_context
|
|
368
|
+
def example(ctx, examples, overwrite):
|
|
369
|
+
"""
|
|
370
|
+
Copy named example(s) from a bundle onto the device. Multiple examples
|
|
371
|
+
can be installed at once by providing more than one example name, each
|
|
372
|
+
separated by a space.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
for example_arg in examples:
|
|
376
|
+
available_examples = get_bundle_examples(
|
|
377
|
+
get_bundles_list(), avoid_download=True
|
|
378
|
+
)
|
|
379
|
+
if example_arg in available_examples:
|
|
380
|
+
filename = available_examples[example_arg].split(os.path.sep)[-1]
|
|
381
|
+
|
|
382
|
+
if overwrite or not ctx.obj["backend"].file_exists(filename):
|
|
383
|
+
click.echo(
|
|
384
|
+
f"{'Copying' if not overwrite else 'Overwriting'}: {filename}"
|
|
385
|
+
)
|
|
386
|
+
ctx.obj["backend"].install_module_py(
|
|
387
|
+
{"path": available_examples[example_arg]}, location=""
|
|
388
|
+
)
|
|
389
|
+
else:
|
|
390
|
+
click.secho(
|
|
391
|
+
f"File: {filename} already exists. Use --overwrite if you wish to replace it.",
|
|
392
|
+
fg="red",
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
click.secho(
|
|
396
|
+
f"Error: {example_arg} was not found in any local bundle examples.",
|
|
397
|
+
fg="red",
|
|
347
398
|
)
|
|
348
399
|
|
|
349
400
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: circup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: A tool to manage/update libraries on CircuitPython devices.
|
|
5
5
|
Home-page: https://github.com/adafruit/circup
|
|
6
6
|
Author: Adafruit Industries
|
|
@@ -57,16 +57,16 @@ Requires-Dist: sphinx; extra == "dev"
|
|
|
57
57
|
Requires-Dist: wheel; extra == "dev"
|
|
58
58
|
Requires-Dist: twine; extra == "dev"
|
|
59
59
|
Provides-Extra: all
|
|
60
|
+
Requires-Dist: pytest; extra == "all"
|
|
61
|
+
Requires-Dist: sphinx; extra == "all"
|
|
60
62
|
Requires-Dist: wheel; extra == "all"
|
|
61
|
-
Requires-Dist: pytest-faulthandler; extra == "all"
|
|
62
63
|
Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
|
|
63
|
-
Requires-Dist:
|
|
64
|
-
Requires-Dist: pytest; extra == "all"
|
|
64
|
+
Requires-Dist: pytest-faulthandler; extra == "all"
|
|
65
65
|
Requires-Dist: coverage; extra == "all"
|
|
66
|
-
Requires-Dist:
|
|
67
|
-
Requires-Dist: pytest-cov; extra == "all"
|
|
66
|
+
Requires-Dist: pylint; extra == "all"
|
|
68
67
|
Requires-Dist: twine; extra == "all"
|
|
69
|
-
Requires-Dist:
|
|
68
|
+
Requires-Dist: pytest-cov; extra == "all"
|
|
69
|
+
Requires-Dist: black; extra == "all"
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
CircUp
|
|
@@ -165,6 +165,8 @@ To get help, just type the command::
|
|
|
165
165
|
--host TEXT Hostname or IP address of a device. Overrides automatic
|
|
166
166
|
path detection.
|
|
167
167
|
--password TEXT Password to use for authentication when --host is used.
|
|
168
|
+
--timeout INTEGER Specify the timeout in seconds for any network
|
|
169
|
+
operations.
|
|
168
170
|
--board-id TEXT Manual Board ID of the CircuitPython device. If provided
|
|
169
171
|
in combination with --cpy-version, it overrides the
|
|
170
172
|
detected board ID.
|
|
@@ -177,6 +179,7 @@ To get help, just type the command::
|
|
|
177
179
|
bundle-add Add bundles to the local bundles list, by "user/repo"...
|
|
178
180
|
bundle-remove Remove one or more bundles from the local bundles list.
|
|
179
181
|
bundle-show Show the list of bundles, default and local, with URL,...
|
|
182
|
+
example Copy named example(s) from a bundle onto the device.
|
|
180
183
|
freeze Output details of all the modules found on the connected...
|
|
181
184
|
install Install a named module(s) onto the device.
|
|
182
185
|
list Lists all out of date modules found on the connected...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -10,16 +10,16 @@ update_checker
|
|
|
10
10
|
importlib_metadata
|
|
11
11
|
|
|
12
12
|
[all]
|
|
13
|
+
pytest
|
|
14
|
+
sphinx
|
|
13
15
|
wheel
|
|
14
|
-
pytest-faulthandler
|
|
15
16
|
pytest-random-order>=1.0.0
|
|
16
|
-
|
|
17
|
-
pytest
|
|
17
|
+
pytest-faulthandler
|
|
18
18
|
coverage
|
|
19
|
-
|
|
20
|
-
pytest-cov
|
|
19
|
+
pylint
|
|
21
20
|
twine
|
|
22
|
-
|
|
21
|
+
pytest-cov
|
|
22
|
+
black
|
|
23
23
|
|
|
24
24
|
[dev]
|
|
25
25
|
pytest
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|