circup 2.2.6__py3-none-any.whl → 2.4.0__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/backends.py CHANGED
@@ -197,7 +197,9 @@ class Backend:
197
197
  # Create the library directory first.
198
198
  self.create_directory(device_path, library_path)
199
199
  if local_path is None:
200
- if pyext:
200
+ # Fallback to the source version (py) if the bundle doesn't have
201
+ # a compiled version (mpy)
202
+ if pyext or bundle.platform is None:
201
203
  # Use Python source for module.
202
204
  self.install_module_py(metadata)
203
205
  else:
@@ -648,9 +650,7 @@ class WebBackend(Backend):
648
650
  if not module_name:
649
651
  # Must be a directory based module.
650
652
  module_name = os.path.basename(os.path.dirname(metadata["path"]))
651
- major_version = self.get_circuitpython_version()[0].split(".")[0]
652
- bundle_platform = "{}mpy".format(major_version)
653
- bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
653
+ bundle_path = os.path.join(bundle.lib_dir(), module_name)
654
654
  if os.path.isdir(bundle_path):
655
655
 
656
656
  self.install_dir_http(bundle_path)
@@ -920,9 +920,7 @@ class DiskBackend(Backend):
920
920
  # Must be a directory based module.
921
921
  module_name = os.path.basename(os.path.dirname(metadata["path"]))
922
922
 
923
- major_version = self.get_circuitpython_version()[0].split(".")[0]
924
- bundle_platform = "{}mpy".format(major_version)
925
- bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
923
+ bundle_path = os.path.join(bundle.lib_dir(), module_name)
926
924
  if os.path.isdir(bundle_path):
927
925
  target_path = os.path.join(self.library_path, module_name)
928
926
  # Copy the directory.
circup/bundle.py CHANGED
@@ -10,22 +10,26 @@ import sys
10
10
  import click
11
11
  import requests
12
12
 
13
+ from semver import VersionInfo
14
+
13
15
  from circup.shared import (
14
16
  DATA_DIR,
15
17
  PLATFORMS,
16
18
  REQUESTS_TIMEOUT,
17
- tags_data_load,
18
19
  get_latest_release_from_url,
19
20
  )
20
21
 
21
22
  from circup.logging import logger
22
23
 
23
24
 
24
- class Bundle:
25
+ class Bundle: # pylint: disable=too-many-instance-attributes
25
26
  """
26
27
  All the links and file names for a bundle
27
28
  """
28
29
 
30
+ #: Avoid requests to the internet
31
+ offline = False
32
+
29
33
  def __init__(self, repo):
30
34
  """
31
35
  Initialise a Bundle created from its github info.
@@ -46,29 +50,42 @@ class Bundle:
46
50
  # tag
47
51
  self._current = None
48
52
  self._latest = None
53
+ self.pinned_tag = None
54
+ self._available = []
55
+ #
56
+ self.platform = None
49
57
 
50
- def lib_dir(self, platform):
58
+ def lib_dir(self, source=False):
51
59
  """
52
- This bundle's lib directory for the platform.
60
+ This bundle's lib directory for the bundle's source or compiled version.
53
61
 
54
- :param str platform: The platform identifier (py/6mpy/...).
55
- :return: The path to the lib directory for the platform.
62
+ :param bool source: Whether to return the path to the source lib
63
+ directory or to :py:attr:`self.platform`'s lib directory. If `source` is
64
+ `False` but :py:attr:`self.platform` is None, the source lib directory
65
+ will be returned instead.
66
+ :return: The path to the lib directory.
56
67
  """
57
68
  tag = self.current_tag
69
+ platform = "py" if source or not self.platform else self.platform
58
70
  return os.path.join(
59
71
  self.dir.format(platform=platform),
60
72
  self.basename.format(platform=PLATFORMS[platform], tag=tag),
61
73
  "lib",
62
74
  )
63
75
 
64
- def examples_dir(self, platform):
76
+ def examples_dir(self, source=False):
65
77
  """
66
- This bundle's examples directory for the platform.
78
+ This bundle's examples directory for the bundle's source or compiled
79
+ version.
67
80
 
68
- :param str platform: The platform identifier (py/6mpy/...).
69
- :return: The path to the examples directory for the platform.
81
+ :param bool source: Whether to return the path to the source examples
82
+ directory or to :py:attr:`self.platform`'s examples directory. If
83
+ `source` is `False` but :py:attr:`self.platform` is None, the source
84
+ examples directory will be returned instead.
85
+ :return: The path to the examples directory.
70
86
  """
71
87
  tag = self.current_tag
88
+ platform = "py" if source or not self.platform else self.platform
72
89
  return os.path.join(
73
90
  self.dir.format(platform=platform),
74
91
  self.basename.format(platform=PLATFORMS[platform], tag=tag),
@@ -99,21 +116,34 @@ class Bundle:
99
116
  @property
100
117
  def current_tag(self):
101
118
  """
102
- Lazy load current cached tag from the BUNDLE_DATA json file.
119
+ The current tag for the project. If the tag hasn't been explicitly set
120
+ this will be the pinned tag, if one is set and it is available. If there
121
+ is no pinned tag, this will be the latest available tag that is locally
122
+ available.
103
123
 
104
- :return: The current cached tag value for the project.
124
+ :return: The current tag value for the project.
105
125
  """
106
126
  if self._current is None:
107
- self._current = tags_data_load(logger).get(self.key, "0")
127
+ if self.pinned_tag:
128
+ self._current = (
129
+ self.pinned_tag if self.pinned_tag in self._available else None
130
+ )
131
+ else:
132
+ self._current = (
133
+ # This represents the latest version locally available
134
+ self._available[-1]
135
+ if len(self._available) > 0
136
+ else None
137
+ )
138
+
108
139
  return self._current
109
140
 
110
141
  @current_tag.setter
111
142
  def current_tag(self, tag):
112
143
  """
113
- Set the current cached tag (after updating).
144
+ Set the current tag (after updating).
114
145
 
115
146
  :param str tag: The new value for the current tag.
116
- :return: The current cached tag value for the project.
117
147
  """
118
148
  self._current = tag
119
149
 
@@ -125,31 +155,109 @@ class Bundle:
125
155
  :return: The most recent tag value for the project.
126
156
  """
127
157
  if self._latest is None:
128
- self._latest = get_latest_release_from_url(
129
- self.url + "/releases/latest", logger
130
- )
158
+ if self.offline:
159
+ self._latest = self._available[-1] if len(self._available) > 0 else None
160
+ else:
161
+ self._latest = get_latest_release_from_url(
162
+ self.url + "/releases/latest", logger
163
+ )
131
164
  return self._latest
132
165
 
166
+ @property
167
+ def available_tags(self):
168
+ """
169
+ The locally available tags to use for the project.
170
+
171
+ :return: All tags available for the project.
172
+ """
173
+ return tuple(self._available)
174
+
175
+ @available_tags.setter
176
+ def available_tags(self, tags):
177
+ """
178
+ Set the available tags.
179
+
180
+ :param str|list tags: The new value for the locally available tags.
181
+ """
182
+ if isinstance(tags, str):
183
+ tags = [tags]
184
+
185
+ try:
186
+ tags = sorted(tags, key=self.parse_version)
187
+ except ValueError as ex:
188
+ logger.warning(
189
+ "Bundle '%s' has invalid tags, cannot order by version.", self.key
190
+ )
191
+ logger.warning(ex)
192
+ self._available = tags
193
+
194
+ def add_tag(self, tag: str) -> None:
195
+ """
196
+ Add a tag to the list of available tags.
197
+
198
+ This will add the tag if it isn't already present in the list of
199
+ available tags. The tag will be added so that the list is sorted in an
200
+ increasing order. This ensures that that last tag is always the latest.
201
+
202
+ :param str tag: The tag to add to the list of available tags.
203
+ """
204
+ if tag in self._available:
205
+ # The tag is already stored for some reason, lets not add it again
206
+ return
207
+
208
+ try:
209
+ version_tag = self.parse_version(tag)
210
+
211
+ for rev_i, available_tag in enumerate(reversed(self._available)):
212
+ available_version_tag = self.parse_version(available_tag)
213
+ if version_tag > available_version_tag:
214
+ i = len(self._available) - rev_i
215
+ self._available.insert(i, tag)
216
+ break
217
+ else:
218
+ self._available.insert(0, tag)
219
+ except ValueError as ex:
220
+ logger.warning(
221
+ "Bundle tag '%s' is not a valid tag, cannot order by version.", tag
222
+ )
223
+ logger.warning(ex)
224
+ self._available.append(tag)
225
+
133
226
  def validate(self):
134
227
  """
135
- Test the existence of the expected URLs (not their content)
228
+ Test the existence of the expected URL (not the content)
136
229
  """
137
230
  tag = self.latest_tag
138
231
  if not tag or tag == "releases":
139
232
  if "--verbose" in sys.argv:
140
233
  click.secho(f' Invalid tag "{tag}"', fg="red")
141
234
  return False
142
- for platform in PLATFORMS.values():
143
- url = self.url_format.format(platform=platform, tag=tag)
144
- r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
145
- # pylint: disable=no-member
146
- if r.status_code != requests.codes.ok:
147
- if "--verbose" in sys.argv:
148
- click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
149
- return False
150
- # pylint: enable=no-member
235
+ url = self.url_format.format(platform="py", tag=tag)
236
+ r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
237
+ # pylint: disable=no-member
238
+ if r.status_code != requests.codes.ok:
239
+ if "--verbose" in sys.argv:
240
+ click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
241
+ return False
242
+ # pylint: enable=no-member
151
243
  return True
152
244
 
245
+ @staticmethod
246
+ def parse_version(tag: str) -> VersionInfo:
247
+ """
248
+ Parse a tag to get a VersionInfo object.
249
+
250
+ `VersionInfo` objects are useful for ordering the tags from oldest to
251
+ newest in :py:attr:`self.available_tags`. The tags are stripped of a
252
+ leading 'v' (if one is present) and minor and patch components are
253
+ optional. This is to allow more flexibility with how a bundle is
254
+ versioned.
255
+
256
+ :param str tag: The tag to parse.
257
+ :return: A `VersionInfo` object parsed from the tag.
258
+ """
259
+ return VersionInfo.parse(tag.removeprefix("v"), optional_minor_and_patch=True)
260
+
153
261
  def __repr__(self):
154
262
  """
155
263
  Helps with log files.
@@ -166,5 +274,7 @@ class Bundle:
166
274
  "url_format": self.url_format,
167
275
  "current": self._current,
168
276
  "latest": self._latest,
277
+ "pinned": self.pinned_tag,
278
+ "available": self._available,
169
279
  }
170
280
  )
circup/command_utils.py CHANGED
@@ -12,10 +12,10 @@ import os
12
12
 
13
13
  from subprocess import check_output
14
14
  import sys
15
- import shutil
16
15
  import zipfile
17
16
  import json
18
17
  import re
18
+ from pathlib import Path
19
19
  import toml
20
20
  import requests
21
21
  import click
@@ -23,13 +23,13 @@ import click
23
23
  from circup.shared import (
24
24
  PLATFORMS,
25
25
  REQUESTS_TIMEOUT,
26
+ SUPPORTED_PLATFORMS,
26
27
  _get_modules_file,
27
28
  BUNDLE_CONFIG_OVERWRITE,
28
29
  BUNDLE_CONFIG_FILE,
29
30
  BUNDLE_CONFIG_LOCAL,
30
31
  BUNDLE_DATA,
31
32
  NOT_MCU_LIBRARIES,
32
- tags_data_load,
33
33
  )
34
34
  from circup.logging import logger
35
35
  from circup.module import Module
@@ -106,7 +106,7 @@ def completion_for_install(ctx, param, incomplete):
106
106
  with the ``circup install`` command.
107
107
  """
108
108
  # pylint: disable=unused-argument
109
- available_modules = get_bundle_versions(get_bundles_list(), avoid_download=True)
109
+ available_modules = get_bundle_versions(get_bundles_list(None), avoid_download=True)
110
110
  module_names = {m.replace(".py", "") for m in available_modules}
111
111
  if incomplete:
112
112
  module_names = [name for name in module_names if name.startswith(incomplete)]
@@ -121,7 +121,9 @@ def completion_for_example(ctx, param, incomplete):
121
121
  """
122
122
 
123
123
  # pylint: disable=unused-argument, consider-iterating-dictionary
124
- available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
124
+ available_examples = get_bundle_examples(
125
+ get_bundles_list(None), avoid_download=True
126
+ )
125
127
  matching_examples = []
126
128
  for term in incomplete:
127
129
  _examples = [
@@ -134,41 +136,143 @@ def completion_for_example(ctx, param, incomplete):
134
136
  return sorted(matching_examples)
135
137
 
136
138
 
137
- def ensure_latest_bundle(bundle):
139
+ def ensure_bundle_tag(bundle, tag):
138
140
  """
139
- Ensure that there's a copy of the latest library bundle available so circup
140
- can check the metadata contained therein.
141
+ Ensure that there's a copy of the library bundle with the version referenced
142
+ by the tag.
141
143
 
142
144
  :param Bundle bundle: the target Bundle object.
145
+ :param tag: the target bundle's tag (version).
146
+
147
+ :return: If the bundle is available.
143
148
  """
144
- logger.info("Checking library updates for %s.", bundle.key)
145
- tag = bundle.latest_tag
146
- do_update = False
147
- if tag == bundle.current_tag:
148
- for platform in PLATFORMS:
149
- # missing directories (new platform added on an existing install
150
- # or side effect of pytest or network errors)
151
- do_update = do_update or not os.path.isdir(bundle.lib_dir(platform))
149
+ if tag is None:
150
+ logger.warning("Bundle version requested is 'None'.")
151
+ return False
152
+
153
+ do_update_source = False
154
+ do_update_compiled = False
155
+ if tag in bundle.available_tags:
156
+ # missing directories (new platform added on an existing install
157
+ # or side effect of pytest or network errors)
158
+ # Check for the source
159
+ do_update_source = not os.path.isdir(bundle.lib_dir(source=True))
160
+ do_update_compiled = bundle.platform is not None and not os.path.isdir(
161
+ bundle.lib_dir(source=False)
162
+ )
152
163
  else:
153
- do_update = True
164
+ do_update_source = True
165
+ do_update_compiled = bundle.platform is not None
166
+
167
+ if not (do_update_source or do_update_compiled):
168
+ logger.info("Current bundle version available (%s).", tag)
169
+ return True
170
+
171
+ if Bundle.offline:
172
+ if do_update_source: # pylint: disable=no-else-return
173
+ logger.info(
174
+ "Bundle version not available but skipping update in offline mode."
175
+ )
176
+ return False
177
+ else:
178
+ logger.info(
179
+ "Bundle platform not available. Falling back to source (.py) files in offline mode."
180
+ )
181
+ bundle.platform = None
182
+ return True
183
+
184
+ logger.info("New version available (%s).", tag)
185
+ if do_update_source:
186
+ try:
187
+ get_bundle(bundle, tag, "py")
188
+ except requests.exceptions.HTTPError as ex:
189
+ click.secho(
190
+ f"There was a problem downloading the 'py' platform for the '{bundle.key}' bundle.",
191
+ fg="red",
192
+ )
193
+ logger.exception(ex)
194
+ return False # Bundle isn't available
195
+ bundle.add_tag(tag)
196
+ tags_data_save_tags(bundle.key, bundle.available_tags)
154
197
 
155
- if do_update:
156
- logger.info("New version available (%s).", tag)
198
+ if do_update_compiled:
157
199
  try:
158
- get_bundle(bundle, tag)
159
- tags_data_save_tag(bundle.key, tag)
200
+ get_bundle(bundle, tag, bundle.platform)
160
201
  except requests.exceptions.HTTPError as ex:
161
- # See #20 for reason for this
162
202
  click.secho(
163
203
  (
164
- "There was a problem downloading that platform bundle. "
165
- "Skipping and using existing download if available."
204
+ f"There was a problem downloading the '{bundle.platform}' platform for the "
205
+ f"'{bundle.key}' bundle.\nFalling back to source (.py) files."
166
206
  ),
167
207
  fg="red",
168
208
  )
169
209
  logger.exception(ex)
210
+ bundle.platform = None # Compiled isn't available, source is good
211
+ bundle.current_tag = tag
212
+
213
+ return True # bundle is available
214
+
215
+
216
+ def ensure_latest_bundle(bundle):
217
+ """
218
+ Ensure that there's a copy of the latest library bundle available so circup
219
+ can check the metadata contained therein.
220
+
221
+ :param Bundle bundle: the target Bundle object.
222
+ """
223
+ logger.info("Checking library updates for %s.", bundle.key)
224
+ tag = bundle.latest_tag
225
+ is_available = ensure_bundle_tag(bundle, tag)
226
+ if is_available:
227
+ click.echo(f"Using latest bundle for {bundle.key} ({tag}).")
170
228
  else:
171
- logger.info("Current bundle up to date %s.", tag)
229
+ if bundle.current_tag is None:
230
+ # See issue #20 for reason for this
231
+ click.secho("Please try again in a moment.", fg="red")
232
+ sys.exit(1)
233
+ else:
234
+ # See PR #184 for reason for this
235
+ click.secho(
236
+ f"Skipping and using existing bundle for {bundle.key} ({bundle.current_tag}).",
237
+ fg="red",
238
+ )
239
+
240
+
241
+ def ensure_pinned_bundle(bundle):
242
+ """
243
+ Ensure that there's a copy of the pinned library bundle available so circup
244
+ can check the metadata contained therein.
245
+
246
+ :param Bundle bundle: the target Bundle object.
247
+ """
248
+ logger.info("Checking library for %s.", bundle.key)
249
+ tag = bundle.pinned_tag
250
+ is_available = ensure_bundle_tag(bundle, tag)
251
+ if is_available:
252
+ click.echo(f"Using pinned bundle for {bundle.key} ({tag}).")
253
+ else:
254
+ click.secho(
255
+ (
256
+ "Check pinned version to make sure it is correct and check "
257
+ f"{bundle.url} to make sure the version ({tag}) exists."
258
+ ),
259
+ fg="red",
260
+ )
261
+ sys.exit(1)
262
+
263
+
264
+ def ensure_bundle(bundle):
265
+ """
266
+ Ensure that there's a copy of either the pinned library bundle, or if no
267
+ version is pinned, the latest library bundle available so circup can check
268
+ the metadata contained therein.
269
+
270
+ :param Bundle bundle: the target Bundle object.
271
+ """
272
+ if bundle.pinned_tag is not None:
273
+ ensure_pinned_bundle(bundle)
274
+ else:
275
+ ensure_latest_bundle(bundle)
172
276
 
173
277
 
174
278
  def find_device():
@@ -293,41 +397,39 @@ def find_modules(backend, bundles_list):
293
397
  # pylint: enable=broad-except,too-many-locals
294
398
 
295
399
 
296
- def get_bundle(bundle, tag):
400
+ def get_bundle(bundle, tag, platform):
297
401
  """
298
402
  Downloads and extracts the version of the bundle with the referenced tag.
299
403
  The resulting zip file is saved on the local filesystem.
300
404
 
301
405
  :param Bundle bundle: the target Bundle object.
302
406
  :param str tag: The GIT tag to use to download the bundle.
407
+ :param str platform: The platform string (i.e. '10mpy').
303
408
  """
304
- click.echo(f"Downloading latest bundles for {bundle.key} ({tag}).")
305
- for platform, github_string in PLATFORMS.items():
306
- # Report the platform: "8.x-mpy", etc.
307
- click.echo(f"{github_string}:")
308
- url = bundle.url_format.format(platform=github_string, tag=tag)
309
- logger.info("Downloading bundle: %s", url)
310
- r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
311
- # pylint: disable=no-member
312
- if r.status_code != requests.codes.ok:
313
- logger.warning("Unable to connect to %s", url)
314
- r.raise_for_status()
315
- # pylint: enable=no-member
316
- total_size = int(r.headers.get("Content-Length"))
317
- temp_zip = bundle.zip.format(platform=platform)
318
- with click.progressbar(
319
- r.iter_content(1024), label="Extracting:", length=total_size
320
- ) as pbar, open(temp_zip, "wb") as zip_fp:
321
- for chunk in pbar:
322
- zip_fp.write(chunk)
323
- pbar.update(len(chunk))
324
- logger.info("Saved to %s", temp_zip)
325
- temp_dir = bundle.dir.format(platform=platform)
326
- if os.path.isdir(temp_dir):
327
- shutil.rmtree(temp_dir)
328
- with zipfile.ZipFile(temp_zip, "r") as zfile:
329
- zfile.extractall(temp_dir)
330
- bundle.current_tag = tag
409
+ click.echo(f"Downloading '{platform}' bundle for {bundle.key} ({tag}).")
410
+ github_string = PLATFORMS[platform]
411
+ # Report the platform: "8.x-mpy", etc.
412
+ click.echo(f"{github_string}:")
413
+ url = bundle.url_format.format(platform=github_string, tag=tag)
414
+ logger.info("Downloading bundle: %s", url)
415
+ r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
416
+ # pylint: disable=no-member
417
+ if r.status_code != requests.codes.ok:
418
+ logger.warning("Unable to connect to %s", url)
419
+ r.raise_for_status()
420
+ # pylint: enable=no-member
421
+ total_size = int(r.headers.get("Content-Length"))
422
+ temp_zip = bundle.zip.format(platform=platform)
423
+ with click.progressbar(
424
+ r.iter_content(1024), label="Extracting:", length=total_size
425
+ ) as pbar, open(temp_zip, "wb") as zip_fp:
426
+ for chunk in pbar:
427
+ zip_fp.write(chunk)
428
+ pbar.update(len(chunk))
429
+ logger.info("Saved to %s", temp_zip)
430
+ temp_dir = bundle.dir.format(platform=platform)
431
+ with zipfile.ZipFile(temp_zip, "r") as zfile:
432
+ zfile.extractall(temp_dir)
331
433
  click.echo("\nOK\n")
332
434
 
333
435
 
@@ -347,9 +449,9 @@ def get_bundle_examples(bundles_list, avoid_download=False):
347
449
 
348
450
  try:
349
451
  for bundle in bundles_list:
350
- if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
351
- ensure_latest_bundle(bundle)
352
- path = bundle.examples_dir("py")
452
+ if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
453
+ ensure_bundle(bundle)
454
+ path = bundle.examples_dir(source=True)
353
455
  meta_saved = os.path.join(path, "../bundle_examples.json")
354
456
  if os.path.exists(meta_saved):
355
457
  with open(meta_saved, "r", encoding="utf-8") as f:
@@ -383,9 +485,10 @@ def get_bundle_examples(bundles_list, avoid_download=False):
383
485
 
384
486
  def get_bundle_versions(bundles_list, avoid_download=False):
385
487
  """
386
- Returns a dictionary of metadata from modules in the latest known release
387
- of the library bundle. Uses the Python version (rather than the compiled
388
- version) of the library modules.
488
+ Returns a dictionary of metadata from modules in either the pinned release
489
+ if one is present in 'pyproject.toml', or the latest known release of the
490
+ library bundle. Uses the Python version (rather than the compiled version)
491
+ of the library modules.
389
492
 
390
493
  :param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
391
494
  :param bool avoid_download: if True, download the bundle only if missing.
@@ -394,9 +497,9 @@ def get_bundle_versions(bundles_list, avoid_download=False):
394
497
  """
395
498
  all_the_modules = dict()
396
499
  for bundle in bundles_list:
397
- if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
398
- ensure_latest_bundle(bundle)
399
- path = bundle.lib_dir("py")
500
+ if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
501
+ ensure_bundle(bundle)
502
+ path = bundle.lib_dir(source=True)
400
503
  path_modules = _get_modules_file(path, logger)
401
504
  for name, module in path_modules.items():
402
505
  module["bundle"] = bundle
@@ -443,14 +546,32 @@ def get_bundles_local_dict():
443
546
  return dict()
444
547
 
445
548
 
446
- def get_bundles_list():
549
+ def get_bundles_list(bundle_tags, platform_version=None):
447
550
  """
448
551
  Retrieve the list of bundles from the config dictionary.
449
552
 
553
+ :param Dict[str,str]|None bundle_tags: Pinned bundle tags. These override
554
+ any tags found in the pyproject.toml.
555
+ :param str platform_version: The platform version needed for the current
556
+ device.
450
557
  :return: List of supported bundles as Bundle objects.
451
558
  """
452
559
  bundle_config = get_bundles_dict()
560
+ tags = tags_data_load()
561
+ pyproject = find_pyproject()
562
+ pinned_tags = (
563
+ pyproject_bundle_versions(pyproject) if pyproject is not None else None
564
+ )
565
+
566
+ if bundle_tags is not None:
567
+ pinned_tags = bundle_tags if pinned_tags is None else pinned_tags | bundle_tags
568
+
453
569
  bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
570
+ for bundle in bundles_list:
571
+ bundle.platform = platform_version
572
+ bundle.available_tags = tags.get(bundle.key, [])
573
+ if pinned_tags is not None:
574
+ bundle.pinned_tag = pinned_tags.get(bundle.key)
454
575
  logger.info("Using bundles: %s", ", ".join(b.key for b in bundles_list))
455
576
  return bundles_list
456
577
 
@@ -613,15 +734,38 @@ def save_local_bundles(bundles_data):
613
734
  os.unlink(BUNDLE_CONFIG_LOCAL)
614
735
 
615
736
 
616
- def tags_data_save_tag(key, tag):
737
+ def tags_data_load():
738
+ """
739
+ Load the list of the version tags of the bundles on disk.
740
+
741
+ :return: a dict() of tags indexed by Bundle identifiers/keys.
617
742
  """
618
- Add or change the saved tag value for a bundle.
743
+ tags_data = None
744
+ try:
745
+ with open(BUNDLE_DATA, encoding="utf-8") as data:
746
+ try:
747
+ tags_data = json.load(data)
748
+ except json.decoder.JSONDecodeError as ex:
749
+ # Sometimes (why?) the JSON file becomes corrupt. In which case
750
+ # log it and carry on as if setting up for first time.
751
+ logger.error("Could not parse %s", BUNDLE_DATA)
752
+ logger.exception(ex)
753
+ except FileNotFoundError:
754
+ pass
755
+ if not isinstance(tags_data, dict):
756
+ tags_data = {}
757
+ return tags_data
758
+
759
+
760
+ def tags_data_save_tags(key, tags):
761
+ """
762
+ Add or change the saved available tags value for a bundle.
619
763
 
620
764
  :param str key: The bundle's identifier/key.
621
- :param str tag: The new tag for the bundle.
765
+ :param List[str] tags: The new tags for the bundle.
622
766
  """
623
- tags_data = tags_data_load(logger)
624
- tags_data[key] = tag
767
+ tags_data = tags_data_load()
768
+ tags_data[key] = tags
625
769
  with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
626
770
  json.dump(tags_data, data)
627
771
 
@@ -855,3 +999,52 @@ def is_virtual_env_active():
855
999
  virtual environment, regardless how circup is installed.
856
1000
  """
857
1001
  return "VIRTUAL_ENV" in os.environ
1002
+
1003
+
1004
+ def find_pyproject():
1005
+ """
1006
+ Look for a pyproject.toml in the current directory or its parent directories.
1007
+
1008
+ :return: The path to the pyproject.toml for the project, or None if it
1009
+ couldn't be found.
1010
+ """
1011
+ logger.info("Looking for pyproject.toml file.")
1012
+ cwd = Path.cwd()
1013
+ candidates = [cwd, cwd.parent]
1014
+
1015
+ for path in candidates:
1016
+ pyproject_file = path / "pyproject.toml"
1017
+
1018
+ if pyproject_file.exists():
1019
+ logger.info("Found pyproject.toml at '%s'", str(pyproject_file))
1020
+ return pyproject_file
1021
+
1022
+ logger.info("No pyproject.toml file found.")
1023
+ return None
1024
+
1025
+
1026
+ def pyproject_bundle_versions(pyproject_file):
1027
+ """
1028
+ Check for specified bundle versions.
1029
+ """
1030
+ pyproject_toml_data = toml.load(pyproject_file)
1031
+ return pyproject_toml_data.get("tool", {}).get("circup", {}).get("bundle-versions")
1032
+
1033
+
1034
+ def parse_cli_bundle_tags(bundle_tags_cli):
1035
+ """Parse bundle tags that were provided from the command line."""
1036
+ bundle_tags = {}
1037
+ for bundle_tag_item in bundle_tags_cli:
1038
+ item = bundle_tag_item.split("=")
1039
+ if len(item) == 2:
1040
+ bundle_tags[item[0].strip()] = item[1].strip()
1041
+ return bundle_tags if len(bundle_tags) > 0 else None
1042
+
1043
+
1044
+ def pretty_supported_cpy_versions():
1045
+ """Return a user friendly string of the supported CircuitPython versions."""
1046
+ supported_cpy = [
1047
+ PLATFORMS[platform].split("-", maxsplit=1)[0]
1048
+ for platform in SUPPORTED_PLATFORMS
1049
+ ]
1050
+ return ", ".join(supported_cpy)
circup/commands.py CHANGED
@@ -24,7 +24,11 @@ import requests
24
24
 
25
25
  from circup.backends import WebBackend, DiskBackend
26
26
  from circup.logging import logger, log_formatter, LOGFILE
27
- from circup.shared import BOARDLESS_COMMANDS, get_latest_release_from_url
27
+ from circup.shared import (
28
+ BOARDLESS_COMMANDS,
29
+ SUPPORTED_PLATFORMS,
30
+ get_latest_release_from_url,
31
+ )
28
32
  from circup.bundle import Bundle
29
33
  from circup.command_utils import (
30
34
  get_device_path,
@@ -37,6 +41,8 @@ from circup.command_utils import (
37
41
  libraries_from_auto_file,
38
42
  get_dependencies,
39
43
  get_bundles_local_dict,
44
+ parse_cli_bundle_tags,
45
+ pretty_supported_cpy_versions,
40
46
  save_local_bundles,
41
47
  get_bundles_dict,
42
48
  completion_for_example,
@@ -67,6 +73,16 @@ from circup.command_utils import (
67
73
  " You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
68
74
  " instead of passing this argument. If both exist the CLI arg takes precedent.",
69
75
  )
76
+ @click.option(
77
+ "--offline",
78
+ is_flag=True,
79
+ help="Prevents Circup from accessing the internet for any reason. "
80
+ "Without this flag, Circup will fail with an error if it needs to access "
81
+ "the network and the network is not available. With this flag, Circup "
82
+ "will attempt to proceed without the network if possible. Circup will "
83
+ "only use bundles downloaded locally even if there might be newer "
84
+ "versions available.",
85
+ )
70
86
  @click.option(
71
87
  "--timeout",
72
88
  default=30,
@@ -84,13 +100,41 @@ from circup.command_utils import (
84
100
  help="Manual CircuitPython version. If provided in combination "
85
101
  "with --board-id, it overrides the detected CPy version.",
86
102
  )
103
+ @click.option(
104
+ "bundle_versions",
105
+ "--bundle-version",
106
+ multiple=True,
107
+ help="Specify the version to use for a bundle. Include the bundle name and "
108
+ "the version separated by '=', similar to the format of requirements.txt. "
109
+ "This option can be used multiple times for different bundles. Bundle "
110
+ "version values provided here will override any pinned values from the "
111
+ "pyproject.toml.",
112
+ )
113
+ @click.option(
114
+ "--allow-unsupported",
115
+ is_flag=True,
116
+ help="Allow using a device with a version of CircuitPython that is no longer "
117
+ "supported. Using an unsupported version of CircuitPython is generally not "
118
+ "recommended because libraries may not work with it.",
119
+ )
87
120
  @click.version_option(
88
121
  prog_name="Circup",
89
122
  message="%(prog)s, A CircuitPython module updater. Version %(version)s",
90
123
  )
91
124
  @click.pass_context
92
125
  def main( # pylint: disable=too-many-locals
93
- ctx, verbose, path, host, port, password, timeout, board_id, cpy_version
126
+ ctx,
127
+ verbose,
128
+ path,
129
+ host,
130
+ port,
131
+ password,
132
+ offline,
133
+ timeout,
134
+ board_id,
135
+ cpy_version,
136
+ bundle_versions,
137
+ allow_unsupported,
94
138
  ): # pragma: no cover
95
139
  """
96
140
  A tool to manage and update libraries on a CircuitPython device.
@@ -98,6 +142,10 @@ def main( # pylint: disable=too-many-locals
98
142
  # pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
99
143
  ctx.ensure_object(dict)
100
144
  ctx.obj["TIMEOUT"] = timeout
145
+ ctx.obj["BUNDLE_TAGS"] = (
146
+ parse_cli_bundle_tags(bundle_versions) if len(bundle_versions) > 0 else None
147
+ )
148
+ Bundle.offline = offline
101
149
 
102
150
  if password is None:
103
151
  password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
@@ -154,20 +202,22 @@ def main( # pylint: disable=too-many-locals
154
202
 
155
203
  logger.info("### Started Circup ###")
156
204
 
157
- # If a newer version of circup is available, print a message.
158
- logger.info("Checking for a newer version of circup")
159
- version = get_circup_version()
160
- if version:
161
- update_checker.update_check("circup", version)
205
+ if offline:
206
+ logger.info(
207
+ "'--offline' flag present, all update checks requiring the network will be skipped."
208
+ )
209
+ else:
210
+ # If a newer version of circup is available, print a message.
211
+ logger.info("Checking for a newer version of circup")
212
+ version = get_circup_version()
213
+ if version:
214
+ update_checker.update_check("circup", version)
162
215
 
163
216
  # stop early if the command is boardless
164
217
  if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
165
218
  return
166
219
 
167
220
  ctx.obj["DEVICE_PATH"] = device_path
168
- latest_version = get_latest_release_from_url(
169
- "https://github.com/adafruit/circuitpython/releases/latest", logger
170
- )
171
221
 
172
222
  if device_path is None or not ctx.obj["backend"].is_device_present():
173
223
  click.secho("Could not find a connected CircuitPython device.", fg="red")
@@ -178,27 +228,54 @@ def main( # pylint: disable=too-many-locals
178
228
  if board_id is None or cpy_version is None
179
229
  else (cpy_version, board_id)
180
230
  )
231
+ major_version = cpy_version.split(".")[0]
232
+ bundle_platform = "{}mpy".format(major_version)
233
+ ctx.obj["DEVICE_PLATFORM_VERSION"] = bundle_platform
181
234
  click.echo(
182
235
  "Found device {} at {}, running CircuitPython {}.".format(
183
236
  board_id, device_path, cpy_version
184
237
  )
185
238
  )
186
- try:
187
- if VersionInfo.parse(cpy_version) < VersionInfo.parse(latest_version):
239
+
240
+ if not offline:
241
+ latest_version = get_latest_release_from_url(
242
+ "https://github.com/adafruit/circuitpython/releases/latest", logger
243
+ )
244
+ try:
245
+ if VersionInfo.parse(cpy_version) < VersionInfo.parse(latest_version):
246
+ click.secho(
247
+ "A newer version of CircuitPython ({}) is available.".format(
248
+ latest_version
249
+ ),
250
+ fg="green",
251
+ )
252
+ if board_id:
253
+ url_download = f"https://circuitpython.org/board/{board_id}"
254
+ else:
255
+ url_download = "https://circuitpython.org/downloads"
256
+ click.secho("Get it here: {}".format(url_download), fg="green")
257
+ except ValueError as ex:
258
+ logger.warning("CircuitPython has incorrect semver value.")
259
+ logger.warning(ex)
260
+
261
+ if not bundle_platform in SUPPORTED_PLATFORMS:
262
+ click.secho(
263
+ "The version of CircuitPython on the device is no longer supported.",
264
+ fg="yellow" if allow_unsupported else "red",
265
+ )
266
+ if allow_unsupported:
188
267
  click.secho(
189
- "A newer version of CircuitPython ({}) is available.".format(
190
- latest_version
191
- ),
192
- fg="green",
268
+ "It is recommended to update to a supported version "
269
+ f"({pretty_supported_cpy_versions()}) to ensure compatability.",
270
+ fg="yellow",
193
271
  )
194
- if board_id:
195
- url_download = f"https://circuitpython.org/board/{board_id}"
196
- else:
197
- url_download = "https://circuitpython.org/downloads"
198
- click.secho("Get it here: {}".format(url_download), fg="green")
199
- except ValueError as ex:
200
- logger.warning("CircuitPython has incorrect semver value.")
201
- logger.warning(ex)
272
+ else:
273
+ click.echo(
274
+ f"If you would like to continue to use version {cpy_version} of CircuitPython, "
275
+ "pass the '--allow-unsupported' flag with this command. Otherwise, update to a "
276
+ f"supported version ({pretty_supported_cpy_versions()}) to ensure compatability.",
277
+ )
278
+ sys.exit(1)
202
279
 
203
280
 
204
281
  @main.command()
@@ -210,7 +287,7 @@ def freeze(ctx, requirement): # pragma: no cover
210
287
  device. Option -r saves output to requirements.txt file
211
288
  """
212
289
  logger.info("Freeze")
213
- modules = find_modules(ctx.obj["backend"], get_bundles_list())
290
+ modules = find_modules(ctx.obj["backend"], get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
214
291
  if modules:
215
292
  output = []
216
293
  for module in modules:
@@ -258,7 +335,12 @@ def list_cli(ctx): # pragma: no cover
258
335
 
259
336
  modules = [
260
337
  m.row
261
- for m in find_modules(ctx.obj["backend"], get_bundles_list())
338
+ for m in find_modules(
339
+ ctx.obj["backend"],
340
+ get_bundles_list(
341
+ ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
342
+ ),
343
+ )
262
344
  if m.outofdate
263
345
  ]
264
346
  if modules:
@@ -334,7 +416,10 @@ def install(
334
416
 
335
417
  # pylint: disable=too-many-branches
336
418
  # TODO: Ensure there's enough space on the device
337
- available_modules = get_bundle_versions(get_bundles_list())
419
+ platform_version = ctx.obj["DEVICE_PLATFORM_VERSION"] if not pyext else None
420
+ available_modules = get_bundle_versions(
421
+ get_bundles_list(ctx.obj["BUNDLE_TAGS"], platform_version)
422
+ )
338
423
  mod_names = {}
339
424
  for module, metadata in available_modules.items():
340
425
  mod_names[module.replace(".py", "").lower()] = metadata
@@ -367,7 +452,7 @@ def install(
367
452
  upgrade,
368
453
  )
369
454
 
370
- if stubs:
455
+ if stubs and not Bundle.offline:
371
456
  # Check we are in a virtual environment
372
457
  if not is_virtual_env_active():
373
458
  if is_global_install_ok is None:
@@ -440,7 +525,7 @@ def example(ctx, examples, op_list, rename, overwrite):
440
525
  else:
441
526
  click.echo("Available example libraries:")
442
527
  available_examples = get_bundle_examples(
443
- get_bundles_list(), avoid_download=True
528
+ get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
444
529
  )
445
530
  lib_names = {
446
531
  str(key.split(os.path.sep)[0]): value
@@ -451,7 +536,7 @@ def example(ctx, examples, op_list, rename, overwrite):
451
536
 
452
537
  for example_arg in examples:
453
538
  available_examples = get_bundle_examples(
454
- get_bundles_list(), avoid_download=True
539
+ get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
455
540
  )
456
541
  if example_arg in available_examples:
457
542
  filename = available_examples[example_arg].split(os.path.sep)[-1]
@@ -485,14 +570,15 @@ def example(ctx, examples, op_list, rename, overwrite):
485
570
 
486
571
  @main.command()
487
572
  @click.argument("match", required=False, nargs=1)
488
- def show(match): # pragma: no cover
573
+ @click.pass_context
574
+ def show(ctx, match): # pragma: no cover
489
575
  """
490
576
  Show a list of available modules in the bundle. These are modules which
491
577
  *could* be installed on the device.
492
578
 
493
579
  If MATCH is specified only matching modules will be listed.
494
580
  """
495
- available_modules = get_bundle_versions(get_bundles_list())
581
+ available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
496
582
  module_names = sorted([m.replace(".py", "") for m in available_modules])
497
583
  if match is not None:
498
584
  match = match.lower()
@@ -555,7 +641,9 @@ def update(ctx, update_all): # pragma: no cover
555
641
  """
556
642
  logger.info("Update")
557
643
  # Grab current modules.
558
- bundles_list = get_bundles_list()
644
+ bundles_list = get_bundles_list(
645
+ ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
646
+ )
559
647
  installed_modules = find_modules(ctx.obj["backend"], bundles_list)
560
648
  modules_to_update = [m for m in installed_modules if m.outofdate]
561
649
 
@@ -654,13 +742,14 @@ def update(ctx, update_all): # pragma: no cover
654
742
 
655
743
  @main.command("bundle-show")
656
744
  @click.option("--modules", is_flag=True, help="List all the modules per bundle.")
657
- def bundle_show(modules):
745
+ @click.pass_context
746
+ def bundle_show(ctx, modules):
658
747
  """
659
- Show the list of bundles, default and local, with URL, current version
660
- and latest version retrieved from the web.
748
+ Show the list of bundles, default and local, with URL, current version,
749
+ available versions, and latest version retrieved from the web.
661
750
  """
662
751
  local_bundles = get_bundles_local_dict().values()
663
- bundles = get_bundles_list()
752
+ bundles = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
664
753
  available_modules = get_bundle_versions(bundles)
665
754
 
666
755
  for bundle in bundles:
@@ -669,7 +758,13 @@ def bundle_show(modules):
669
758
  else:
670
759
  click.secho(bundle.key, fg="green")
671
760
  click.echo(" " + bundle.url)
672
- click.echo(" version = " + bundle.current_tag)
761
+ click.echo(
762
+ " version = "
763
+ + bundle.current_tag
764
+ + (" (pinned)" if bundle.pinned_tag is not None else "")
765
+ )
766
+ click.echo(" available versions:")
767
+ click.echo(" " + "\n ".join(bundle.available_tags))
673
768
  if modules:
674
769
  click.echo("Modules:")
675
770
  for name, mod in sorted(available_modules.items()):
@@ -695,6 +790,10 @@ def bundle_add(ctx, bundle):
695
790
  )
696
791
  return
697
792
 
793
+ if Bundle.offline:
794
+ click.secho("Cannot add new bundle when '--offline' flag is present.", fg="red")
795
+ return
796
+
698
797
  bundles_dict = get_bundles_local_dict()
699
798
  modified = False
700
799
  for bundle_repo in bundle:
@@ -739,7 +838,7 @@ def bundle_add(ctx, bundle):
739
838
  # save the bundles list
740
839
  save_local_bundles(bundles_dict)
741
840
  # update and get the new bundles for the first time
742
- get_bundle_versions(get_bundles_list())
841
+ get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
743
842
 
744
843
 
745
844
  @main.command("bundle-remove")
@@ -788,3 +887,40 @@ def bundle_remove(bundle, reset):
788
887
  )
789
888
  if modified:
790
889
  save_local_bundles(bundles_local_dict)
890
+
891
+
892
+ @main.command()
893
+ @click.pass_context
894
+ def bundle_freeze(ctx): # pragma: no cover
895
+ """
896
+ Output details of all the bundles for modules found on the connected
897
+ CIRCUITPYTHON device. Copying the output into pyproject.toml will pin the
898
+ bundles.
899
+ """
900
+ logger.info("Bundle Freeze")
901
+ device_modules = ctx.obj["backend"].get_device_versions()
902
+ if not device_modules:
903
+ click.echo("No modules found on the device.")
904
+ return
905
+
906
+ available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
907
+ bundles_used = {}
908
+ for name in device_modules:
909
+ module = available_modules.get(name)
910
+ if module:
911
+ bundle = module["bundle"]
912
+ bundles_used[bundle.key] = bundle.current_tag
913
+
914
+ if bundles_used:
915
+ click.echo(
916
+ "Copy the following lines into your pyproject.toml to pin "
917
+ "the bundles used with modules on the device:\n"
918
+ )
919
+ output = ["[tool.circup.bundle-versions]"]
920
+ for bundle_name, version in bundles_used.items():
921
+ output.append(f'"{bundle_name}" = "{version}"')
922
+ for line in output:
923
+ click.echo(line)
924
+ logger.info(line)
925
+ else:
926
+ click.echo("No bundles used with the modules on the device.")
circup/module.py CHANGED
@@ -79,16 +79,8 @@ class Module:
79
79
  self.max_version = compatibility[1]
80
80
  # Figure out the bundle path.
81
81
  self.bundle_path = None
82
- if self.mpy:
83
- # Byte compiled, now check CircuitPython version.
84
-
85
- major_version = self.backend.get_circuitpython_version()[0].split(".")[0]
86
- bundle_platform = "{}mpy".format(major_version)
87
- else:
88
- # Regular Python
89
- bundle_platform = "py"
90
82
  # module path in the bundle
91
- search_path = bundle.lib_dir(bundle_platform)
83
+ search_path = bundle.lib_dir(source=not self.mpy)
92
84
  if self.file:
93
85
  self.bundle_path = os.path.join(search_path, self.file)
94
86
  else:
circup/shared.py CHANGED
@@ -9,7 +9,6 @@ and Backend class functions.
9
9
  import glob
10
10
  import os
11
11
  import re
12
- import json
13
12
  import importlib.resources
14
13
  import appdirs
15
14
  import requests
@@ -23,7 +22,10 @@ BAD_FILE_FORMAT = "Invalid"
23
22
  DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
24
23
 
25
24
  #: Module formats list (and the other form used in github files)
26
- PLATFORMS = {"py": "py", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
25
+ PLATFORMS = {"py": "py", "8mpy": "8.x-mpy", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
26
+
27
+ #: CircuitPython platforms that are currently supported.
28
+ SUPPORTED_PLATFORMS = ["9mpy", "10mpy"]
27
29
 
28
30
  #: Timeout for requests calls like get()
29
31
  REQUESTS_TIMEOUT = 30
@@ -192,29 +194,6 @@ def extract_metadata(path, logger):
192
194
  return result
193
195
 
194
196
 
195
- def tags_data_load(logger):
196
- """
197
- Load the list of the version tags of the bundles on disk.
198
-
199
- :return: a dict() of tags indexed by Bundle identifiers/keys.
200
- """
201
- tags_data = None
202
- try:
203
- with open(BUNDLE_DATA, encoding="utf-8") as data:
204
- try:
205
- tags_data = json.load(data)
206
- except json.decoder.JSONDecodeError as ex:
207
- # Sometimes (why?) the JSON file becomes corrupt. In which case
208
- # log it and carry on as if setting up for first time.
209
- logger.error("Could not parse %s", BUNDLE_DATA)
210
- logger.exception(ex)
211
- except FileNotFoundError:
212
- pass
213
- if not isinstance(tags_data, dict):
214
- tags_data = {}
215
- return tags_data
216
-
217
-
218
197
  def get_latest_release_from_url(url, logger):
219
198
  """
220
199
  Find the tag name of the latest release by using HTTP HEAD and decoding the redirect.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.2.6
3
+ Version: 2.4.0
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
@@ -0,0 +1,21 @@
1
+ circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
2
+ circup/backends.py,sha256=aH76wEusVCf2IeOCWrypHd2_tBo286TxOBkgYGwXeOI,39869
3
+ circup/bundle.py,sha256=qqDz0MjezKfSngldk8zNahNhPikuOZLAMwBUv7w75Mc,9442
4
+ circup/command_utils.py,sha256=k18vZe0ecAKANoB9SoXh39S-DjE_cC8rpZpr3-gyQ1I,37121
5
+ circup/commands.py,sha256=vAsmWyvTB3sjH4bdfGe9IQxPt3DC925mWdHNRDOeK00,34042
6
+ circup/lazy_metadata.py,sha256=69VidxGGWE13QwAAtMCPNTXTsQ2q5dJvMtclw4YaqEY,3764
7
+ circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
8
+ circup/module.py,sha256=ZrgZhBKuDWHfw9FzcXKjBYx-oybbDnH_IAdek3aSGwU,7131
9
+ circup/shared.py,sha256=d1imapk_esiuz6dMLe37Ck8lHW6W6L3djFpH8KBTAMk,8752
10
+ circup/config/bundle_config.json,sha256=zzpmfy0jD7TxpOOw2P_gqIISuMBG0enb_f3_VneQ5mI,135
11
+ circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
12
+ circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,3617
13
+ circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
14
+ circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
15
+ circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
16
+ circup-2.4.0.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
17
+ circup-2.4.0.dist-info/METADATA,sha256=iqTeOcWnEgpLE8jfAK8c05pqg8LRa1t74lnEnoHxuYM,13617
18
+ circup-2.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
19
+ circup-2.4.0.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
20
+ circup-2.4.0.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
21
+ circup-2.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,21 +0,0 @@
1
- circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
2
- circup/backends.py,sha256=g9Q9xCGZidwsEDL2Ga2cm50YYB54IiqlKUPcxj-pWZA,40008
3
- circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
4
- circup/command_utils.py,sha256=_bYFfsfXoy6ERyTSmqt-RTNuIJN9ilYolKGTF42ebyk,30676
5
- circup/commands.py,sha256=3clk6B6oF5noA31TnuCyzIHNxxFcsoxBCVBOoy-K8fo,29082
6
- circup/lazy_metadata.py,sha256=69VidxGGWE13QwAAtMCPNTXTsQ2q5dJvMtclw4YaqEY,3764
7
- circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
8
- circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
9
- circup/shared.py,sha256=rribEZdoeyZRHxLiezyrDH0vb8DIP4bGOfMpRJm9M9w,9405
10
- circup/config/bundle_config.json,sha256=zzpmfy0jD7TxpOOw2P_gqIISuMBG0enb_f3_VneQ5mI,135
11
- circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
12
- circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,3617
13
- circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
14
- circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
15
- circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
16
- circup-2.2.6.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
17
- circup-2.2.6.dist-info/METADATA,sha256=bBGt9xAlnkAhdypwfT63GgbGSjfzee1JwHJ5FFC2Euw,13617
18
- circup-2.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- circup-2.2.6.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
20
- circup-2.2.6.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
21
- circup-2.2.6.dist-info/RECORD,,