circup 1.6.2__tar.gz → 1.8.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.
Files changed (80) hide show
  1. {circup-1.6.2 → circup-1.8.0}/.gitignore +2 -0
  2. {circup-1.6.2/circup.egg-info → circup-1.8.0}/PKG-INFO +11 -8
  3. {circup-1.6.2 → circup-1.8.0}/README.rst +3 -0
  4. circup-1.8.0/circup/__init__.py +26 -0
  5. {circup-1.6.2 → circup-1.8.0}/circup/backends.py +75 -23
  6. circup-1.8.0/circup/bundle.py +170 -0
  7. circup-1.8.0/circup/command_utils.py +616 -0
  8. circup-1.8.0/circup/commands.py +708 -0
  9. circup-1.8.0/circup/logging.py +33 -0
  10. circup-1.8.0/circup/module.py +209 -0
  11. {circup-1.6.2 → circup-1.8.0}/circup/shared.py +75 -1
  12. {circup-1.6.2 → circup-1.8.0/circup.egg-info}/PKG-INFO +11 -8
  13. {circup-1.6.2 → circup-1.8.0}/circup.egg-info/SOURCES.txt +9 -1
  14. {circup-1.6.2 → circup-1.8.0}/docs/conf.py +1 -1
  15. circup-1.8.0/optional_requirements.txt +4 -0
  16. circup-1.8.0/optional_requirements.txt.license +3 -0
  17. circup-1.8.0/requirements.txt +7 -0
  18. {circup-1.6.2 → circup-1.8.0}/tests/mock_device/boot_out.txt +1 -1
  19. circup-1.8.0/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  20. {circup-1.6.2 → circup-1.8.0}/tests/test_circup.py +198 -197
  21. circup-1.6.2/circup/__init__.py +0 -1584
  22. circup-1.6.2/requirements.txt +0 -58
  23. {circup-1.6.2 → circup-1.8.0}/.github/ISSUE_TEMPLATE.md +0 -0
  24. {circup-1.6.2 → circup-1.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {circup-1.6.2 → circup-1.8.0}/.github/workflows/build.yml +0 -0
  26. {circup-1.6.2 → circup-1.8.0}/.github/workflows/release.yml +0 -0
  27. {circup-1.6.2 → circup-1.8.0}/.isort.cfg +0 -0
  28. {circup-1.6.2 → circup-1.8.0}/.pre-commit-config.yaml +0 -0
  29. {circup-1.6.2 → circup-1.8.0}/.pylintrc +0 -0
  30. {circup-1.6.2 → circup-1.8.0}/CODE_OF_CONDUCT.rst +0 -0
  31. {circup-1.6.2 → circup-1.8.0}/CODE_OF_CONDUCT.rst.license +0 -0
  32. {circup-1.6.2 → circup-1.8.0}/CONTRIBUTING.rst +0 -0
  33. {circup-1.6.2 → circup-1.8.0}/CONTRIBUTING.rst.license +0 -0
  34. {circup-1.6.2 → circup-1.8.0}/LICENSE +0 -0
  35. {circup-1.6.2 → circup-1.8.0}/LICENSES/CC-BY-4.0.txt +0 -0
  36. {circup-1.6.2 → circup-1.8.0}/LICENSES/MIT.txt +0 -0
  37. {circup-1.6.2 → circup-1.8.0}/LICENSES/Unlicense.txt +0 -0
  38. {circup-1.6.2 → circup-1.8.0}/README.rst.license +0 -0
  39. {circup-1.6.2 → circup-1.8.0}/circup/config/bundle_config.json +0 -0
  40. {circup-1.6.2 → circup-1.8.0}/circup/config/bundle_config.json.license +0 -0
  41. {circup-1.6.2 → circup-1.8.0}/circup.egg-info/dependency_links.txt +0 -0
  42. {circup-1.6.2 → circup-1.8.0}/circup.egg-info/entry_points.txt +0 -0
  43. {circup-1.6.2 → circup-1.8.0}/circup.egg-info/requires.txt +7 -7
  44. {circup-1.6.2 → circup-1.8.0}/circup.egg-info/top_level.txt +0 -0
  45. {circup-1.6.2 → circup-1.8.0}/docs/_static/favicon.ico +0 -0
  46. {circup-1.6.2 → circup-1.8.0}/docs/_static/favicon.ico.license +0 -0
  47. {circup-1.6.2 → circup-1.8.0}/docs/index.rst +0 -0
  48. {circup-1.6.2 → circup-1.8.0}/docs/index.rst.license +0 -0
  49. {circup-1.6.2 → circup-1.8.0}/docs/logo.png +0 -0
  50. {circup-1.6.2 → circup-1.8.0}/docs/logo.png.license +0 -0
  51. {circup-1.6.2 → circup-1.8.0}/readthedocs.yml +0 -0
  52. {circup-1.6.2 → circup-1.8.0}/requirements.txt.license +0 -0
  53. {circup-1.6.2 → circup-1.8.0}/setup.cfg +0 -0
  54. {circup-1.6.2 → circup-1.8.0}/setup.py +0 -0
  55. {circup-1.6.2 → circup-1.8.0}/tests/__init__.py +0 -0
  56. {circup-1.6.2 → circup-1.8.0}/tests/bad_module/__init__.py +0 -0
  57. {circup-1.6.2 → circup-1.8.0}/tests/bad_module/my_module.py +0 -0
  58. {circup-1.6.2 → circup-1.8.0}/tests/bad_python.py +0 -0
  59. {circup-1.6.2 → circup-1.8.0}/tests/bundle.json +0 -0
  60. {circup-1.6.2 → circup-1.8.0}/tests/bundle.json.license +0 -0
  61. {circup-1.6.2 → circup-1.8.0}/tests/device.json +0 -0
  62. {circup-1.6.2 → circup-1.8.0}/tests/device.json.license +0 -0
  63. {circup-1.6.2 → circup-1.8.0}/tests/dir_module/__init__.py +0 -0
  64. {circup-1.6.2 → circup-1.8.0}/tests/dir_module/my_module.py +0 -0
  65. {circup-1.6.2 → circup-1.8.0}/tests/import_styles.py +0 -0
  66. {circup-1.6.2 → circup-1.8.0}/tests/local_module.py +0 -0
  67. {circup-1.6.2 → circup-1.8.0}/tests/local_module_cp7.mpy +0 -0
  68. {circup-1.6.2 → circup-1.8.0}/tests/local_module_cp7.mpy.license +0 -0
  69. {circup-1.6.2 → circup-1.8.0}/tests/mock_device/boot_out.txt.license +0 -0
  70. {circup-1.6.2 → circup-1.8.0}/tests/mount_exists.txt +0 -0
  71. {circup-1.6.2 → circup-1.8.0}/tests/mount_exists.txt.license +0 -0
  72. {circup-1.6.2 → circup-1.8.0}/tests/mount_missing.txt +0 -0
  73. {circup-1.6.2 → circup-1.8.0}/tests/mount_missing.txt.license +0 -0
  74. {circup-1.6.2 → circup-1.8.0}/tests/remote_module.py +0 -0
  75. {circup-1.6.2 → circup-1.8.0}/tests/test_bundle_config.json +0 -0
  76. {circup-1.6.2 → circup-1.8.0}/tests/test_bundle_config.json.license +0 -0
  77. {circup-1.6.2 → circup-1.8.0}/tests/test_bundle_config_local.json +0 -0
  78. {circup-1.6.2 → circup-1.8.0}/tests/test_bundle_config_local.json.license +0 -0
  79. {circup-1.6.2 → circup-1.8.0}/tests/test_module.mpy +0 -0
  80. {circup-1.6.2 → circup-1.8.0}/tests/test_module.mpy.license +0 -0
@@ -19,6 +19,7 @@ downloads/
19
19
  eggs/
20
20
  .eggs/
21
21
  lib/
22
+ !tests/mock_device/lib/
22
23
  lib64/
23
24
  parts/
24
25
  sdist/
@@ -93,6 +94,7 @@ venv/
93
94
  ENV/
94
95
  env.bak/
95
96
  venv.bak/
97
+ *_venv/
96
98
 
97
99
  # Spyder project settings
98
100
  .spyderproject
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: circup
3
- Version: 1.6.2
3
+ Version: 1.8.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: black; extra == "all"
61
- Requires-Dist: pytest-cov; extra == "all"
62
- Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
63
- Requires-Dist: sphinx; extra == "all"
64
- Requires-Dist: coverage; extra == "all"
65
- Requires-Dist: twine; extra == "all"
66
60
  Requires-Dist: wheel; extra == "all"
67
61
  Requires-Dist: pytest-faulthandler; extra == "all"
68
- Requires-Dist: pytest; extra == "all"
62
+ Requires-Dist: coverage; extra == "all"
69
63
  Requires-Dist: pylint; extra == "all"
64
+ Requires-Dist: pytest; extra == "all"
65
+ Requires-Dist: twine; extra == "all"
66
+ Requires-Dist: sphinx; extra == "all"
67
+ Requires-Dist: black; extra == "all"
68
+ Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
69
+ Requires-Dist: pytest-cov; 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...
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ CircUp -- a utility to manage and update libraries on a CircuitPython device.
6
+ """
7
+
8
+
9
+ from circup.shared import DATA_DIR, BAD_FILE_FORMAT, extract_metadata, _get_modules_file
10
+ from circup.backends import WebBackend, DiskBackend
11
+ from circup.logging import logger
12
+
13
+
14
+ # Useful constants.
15
+
16
+
17
+ __version__ = "0.0.0-auto.0"
18
+ __repo__ = "https://github.com/adafruit/circup.git"
19
+
20
+
21
+ from circup.commands import *
22
+
23
+ # Allows execution via `python -m circup ...`
24
+ # pylint: disable=no-value-for-parameter
25
+ if __name__ == "__main__": # pragma: no cover
26
+ main()
@@ -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,13 +78,13 @@ class Backend:
79
78
  """
80
79
  raise NotImplementedError
81
80
 
82
- def _install_module_py(self, metadata):
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 _install_module_mpy(self, bundle, metadata):
87
+ def install_module_mpy(self, bundle, metadata):
89
88
  """
90
89
  To be overridden by subclass
91
90
  """
@@ -93,7 +92,7 @@ class Backend:
93
92
 
94
93
  # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks
95
94
  def install_module(
96
- self, device_path, device_modules, name, pyext, mod_names
95
+ self, device_path, device_modules, name, pyext, mod_names, upgrade=False
97
96
  ): # pragma: no cover
98
97
  """
99
98
  Finds a connected device and installs a given module name if it
@@ -108,14 +107,27 @@ class Backend:
108
107
  source or from a pre-compiled module
109
108
  :param mod_names: Dictionary of metadata from modules that can be generated
110
109
  with get_bundle_versions()
110
+ :param bool upgrade: Upgrade the specified modules if they're already installed.
111
111
  """
112
112
  if not name:
113
113
  click.echo("No module name(s) provided.")
114
114
  elif name in mod_names:
115
115
  # Grab device modules to check if module already installed
116
116
  if name in device_modules:
117
- click.echo("'{}' is already installed.".format(name))
118
- return
117
+ if not upgrade:
118
+ # skip already installed modules if no -upgrade flag
119
+ click.echo("'{}' is already installed.".format(name))
120
+ return
121
+
122
+ # uninstall the module before installing
123
+ name = name.lower()
124
+ _mod_names = {}
125
+ for module_item, _metadata in device_modules.items():
126
+ _mod_names[module_item.replace(".py", "").lower()] = _metadata
127
+ if name in _mod_names:
128
+ _metadata = _mod_names[name]
129
+ module_path = _metadata["path"]
130
+ self.uninstall(device_path, module_path)
119
131
 
120
132
  library_path = (
121
133
  os.path.join(device_path, self.LIB_DIR_PATH)
@@ -159,10 +171,10 @@ class Backend:
159
171
 
160
172
  if pyext:
161
173
  # Use Python source for module.
162
- self._install_module_py(metadata)
174
+ self.install_module_py(metadata)
163
175
  else:
164
176
  # Use pre-compiled mpy modules.
165
- self._install_module_mpy(bundle, metadata)
177
+ self.install_module_mpy(bundle, metadata)
166
178
  click.echo("Installed '{}'.".format(name))
167
179
  else:
168
180
  click.echo("Unknown module named, '{}'.".format(name))
@@ -219,6 +231,12 @@ class Backend:
219
231
  board_id = ""
220
232
  return circuit_python, board_id
221
233
 
234
+ def file_exists(self, filepath):
235
+ """
236
+ To be overriden by subclass
237
+ """
238
+ raise NotImplementedError
239
+
222
240
 
223
241
  def _writeable_error():
224
242
  click.secho(
@@ -250,7 +268,8 @@ class WebBackend(Backend):
250
268
  else "Could not find or connect to specified device"
251
269
  ) from exc
252
270
 
253
- self.LIB_DIR_PATH = "fs/lib/"
271
+ self.FS_PATH = "fs/"
272
+ self.LIB_DIR_PATH = f"{self.FS_PATH}lib/"
254
273
  self.host = host
255
274
  self.password = password
256
275
  self.device_location = f"http://:{self.password}@{self.host}"
@@ -260,14 +279,20 @@ class WebBackend(Backend):
260
279
  self.library_path = self.device_location + "/" + self.LIB_DIR_PATH
261
280
  self.timeout = timeout
262
281
 
263
- def install_file_http(self, source):
282
+ def install_file_http(self, source, location=None):
264
283
  """
265
284
  Install file to device using web workflow.
266
285
  :param source source file.
286
+ :param location the location on the device to copy the source
287
+ directory in to. If omitted is CIRCUITPY/lib/ used.
267
288
  """
268
289
  file_name = source.split(os.path.sep)
269
290
  file_name = file_name[-2] if file_name[-1] == "" else file_name[-1]
270
- target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
291
+
292
+ if location is None:
293
+ target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
294
+ else:
295
+ target = self.device_location + "/" + self.FS_PATH + location + file_name
271
296
 
272
297
  auth = HTTPBasicAuth("", self.password)
273
298
 
@@ -277,14 +302,19 @@ class WebBackend(Backend):
277
302
  _writeable_error()
278
303
  r.raise_for_status()
279
304
 
280
- def install_dir_http(self, source):
305
+ def install_dir_http(self, source, location=None):
281
306
  """
282
307
  Install directory to device using web workflow.
283
308
  :param source source directory.
309
+ :param location the location on the device to copy the source
310
+ directory in to. If omitted is CIRCUITPY/lib/ used.
284
311
  """
285
312
  mod_name = source.split(os.path.sep)
286
313
  mod_name = mod_name[-2] if mod_name[-1] == "" else mod_name[-1]
287
- target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
314
+ if location is None:
315
+ target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
316
+ else:
317
+ target = self.device_location + "/" + self.FS_PATH + location + mod_name
288
318
  target = target + "/" if target[:-1] != "/" else target
289
319
  url = urlparse(target)
290
320
  auth = HTTPBasicAuth("", url.password)
@@ -490,7 +520,7 @@ class WebBackend(Backend):
490
520
  _writeable_error()
491
521
  r.raise_for_status()
492
522
 
493
- def _install_module_mpy(self, bundle, metadata):
523
+ def install_module_mpy(self, bundle, metadata):
494
524
  """
495
525
  :param bundle library bundle.
496
526
  :param library_path library path
@@ -514,7 +544,7 @@ class WebBackend(Backend):
514
544
  raise IOError("Cannot find compiled version of module.")
515
545
 
516
546
  # pylint: enable=too-many-locals,too-many-branches
517
- def _install_module_py(self, metadata):
547
+ def install_module_py(self, metadata, location=None):
518
548
  """
519
549
  :param library_path library path
520
550
  :param metadata dictionary.
@@ -522,10 +552,10 @@ class WebBackend(Backend):
522
552
 
523
553
  source_path = metadata["path"] # Path to Python source version.
524
554
  if os.path.isdir(source_path):
525
- self.install_dir_http(source_path)
555
+ self.install_dir_http(source_path, location=location)
526
556
 
527
557
  else:
528
- self.install_file_http(source_path)
558
+ self.install_file_http(source_path, location=location)
529
559
 
530
560
  def get_auto_file_path(self, auto_file_path):
531
561
  """
@@ -560,6 +590,18 @@ class WebBackend(Backend):
560
590
  """
561
591
  self._update_http(module)
562
592
 
593
+ def file_exists(self, filepath):
594
+ """
595
+ return True if the file exists, otherwise False.
596
+ """
597
+ auth = HTTPBasicAuth("", self.password)
598
+ resp = requests.get(
599
+ self.get_file_path(filepath), auth=auth, timeout=self.timeout
600
+ )
601
+ if resp.status_code == 200:
602
+ return True
603
+ return False
604
+
563
605
  def _update_http(self, module):
564
606
  """
565
607
  Update the module using web workflow.
@@ -733,10 +775,9 @@ class DiskBackend(Backend):
733
775
  if not os.path.exists(library_path): # pragma: no cover
734
776
  os.makedirs(library_path)
735
777
 
736
- def _install_module_mpy(self, bundle, metadata):
778
+ def install_module_mpy(self, bundle, metadata):
737
779
  """
738
780
  :param bundle library bundle.
739
- :param library_path library path
740
781
  :param metadata dictionary.
741
782
  """
742
783
  module_name = os.path.basename(metadata["path"]).replace(".py", ".mpy")
@@ -763,21 +804,26 @@ class DiskBackend(Backend):
763
804
  raise IOError("Cannot find compiled version of module.")
764
805
 
765
806
  # pylint: enable=too-many-locals,too-many-branches
766
- def _install_module_py(self, metadata):
807
+ def install_module_py(self, metadata, location=None):
767
808
  """
768
- :param library_path library path
769
809
  :param metadata dictionary.
810
+ :param location the location on the device to copy the py module to.
811
+ If omitted is CIRCUITPY/lib/ used.
770
812
  """
813
+ if location is None:
814
+ location = self.library_path
815
+ else:
816
+ location = os.path.join(self.device_location, location)
771
817
 
772
818
  source_path = metadata["path"] # Path to Python source version.
773
819
  if os.path.isdir(source_path):
774
820
  target = os.path.basename(os.path.dirname(source_path))
775
- target_path = os.path.join(self.library_path, target)
821
+ target_path = os.path.join(location, target)
776
822
  # Copy the directory.
777
823
  shutil.copytree(source_path, target_path)
778
824
  else:
779
825
  target = os.path.basename(source_path)
780
- target_path = os.path.join(self.library_path, target)
826
+ target_path = os.path.join(location, target)
781
827
  # Copy file.
782
828
  shutil.copyfile(source_path, target_path)
783
829
 
@@ -825,6 +871,12 @@ class DiskBackend(Backend):
825
871
  os.remove(module.path)
826
872
  shutil.copyfile(module.bundle_path, module.path)
827
873
 
874
+ def file_exists(self, filepath):
875
+ """
876
+ return True if the file exists, otherwise False.
877
+ """
878
+ return os.path.exists(os.path.join(self.device_location, filepath))
879
+
828
880
  def get_file_path(self, filename):
829
881
  """
830
882
  returns the full path on the device to a given file name.
@@ -0,0 +1,170 @@
1
+ # SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ Class that represents a specific release of a Bundle.
6
+ """
7
+ import os
8
+ import sys
9
+
10
+ import click
11
+ import requests
12
+
13
+ from circup.shared import (
14
+ DATA_DIR,
15
+ PLATFORMS,
16
+ REQUESTS_TIMEOUT,
17
+ tags_data_load,
18
+ get_latest_release_from_url,
19
+ )
20
+
21
+ from circup.logging import logger
22
+
23
+
24
+ class Bundle:
25
+ """
26
+ All the links and file names for a bundle
27
+ """
28
+
29
+ def __init__(self, repo):
30
+ """
31
+ Initialise a Bundle created from its github info.
32
+ Construct all the strings in one place.
33
+
34
+ :param str repo: Repository string for github: "user/repository"
35
+ """
36
+ vendor, bundle_id = repo.split("/")
37
+ bundle_id = bundle_id.lower().replace("_", "-")
38
+ self.key = repo
39
+ #
40
+ self.url = "https://github.com/" + repo
41
+ self.basename = bundle_id + "-{platform}-{tag}"
42
+ self.urlzip = self.basename + ".zip"
43
+ self.dir = os.path.join(DATA_DIR, vendor, bundle_id + "-{platform}")
44
+ self.zip = os.path.join(DATA_DIR, bundle_id + "-{platform}.zip")
45
+ self.url_format = self.url + "/releases/download/{tag}/" + self.urlzip
46
+ # tag
47
+ self._current = None
48
+ self._latest = None
49
+
50
+ def lib_dir(self, platform):
51
+ """
52
+ This bundle's lib directory for the platform.
53
+
54
+ :param str platform: The platform identifier (py/6mpy/...).
55
+ :return: The path to the lib directory for the platform.
56
+ """
57
+ tag = self.current_tag
58
+ return os.path.join(
59
+ self.dir.format(platform=platform),
60
+ self.basename.format(platform=PLATFORMS[platform], tag=tag),
61
+ "lib",
62
+ )
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
+
78
+ def requirements_for(self, library_name, toml_file=False):
79
+ """
80
+ The requirements file for this library.
81
+
82
+ :param str library_name: The name of the library.
83
+ :return: The path to the requirements.txt file.
84
+ """
85
+ platform = "py"
86
+ tag = self.current_tag
87
+ found_file = os.path.join(
88
+ self.dir.format(platform=platform),
89
+ self.basename.format(platform=PLATFORMS[platform], tag=tag),
90
+ "requirements",
91
+ library_name,
92
+ "requirements.txt" if not toml_file else "pyproject.toml",
93
+ )
94
+ if os.path.isfile(found_file):
95
+ with open(found_file, "r", encoding="utf-8") as read_this:
96
+ return read_this.read()
97
+ return None
98
+
99
+ @property
100
+ def current_tag(self):
101
+ """
102
+ Lazy load current cached tag from the BUNDLE_DATA json file.
103
+
104
+ :return: The current cached tag value for the project.
105
+ """
106
+ if self._current is None:
107
+ self._current = tags_data_load(logger).get(self.key, "0")
108
+ return self._current
109
+
110
+ @current_tag.setter
111
+ def current_tag(self, tag):
112
+ """
113
+ Set the current cached tag (after updating).
114
+
115
+ :param str tag: The new value for the current tag.
116
+ :return: The current cached tag value for the project.
117
+ """
118
+ self._current = tag
119
+
120
+ @property
121
+ def latest_tag(self):
122
+ """
123
+ Lazy find the value of the latest tag for the bundle.
124
+
125
+ :return: The most recent tag value for the project.
126
+ """
127
+ if self._latest is None:
128
+ self._latest = get_latest_release_from_url(
129
+ self.url + "/releases/latest", logger
130
+ )
131
+ return self._latest
132
+
133
+ def validate(self):
134
+ """
135
+ Test the existence of the expected URLs (not their content)
136
+ """
137
+ tag = self.latest_tag
138
+ if not tag or tag == "releases":
139
+ if "--verbose" in sys.argv:
140
+ click.secho(f' Invalid tag "{tag}"', fg="red")
141
+ 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
151
+ return True
152
+
153
+ def __repr__(self):
154
+ """
155
+ Helps with log files.
156
+
157
+ :return: A repr of a dictionary containing the Bundles's metadata.
158
+ """
159
+ return repr(
160
+ {
161
+ "key": self.key,
162
+ "url": self.url,
163
+ "urlzip": self.urlzip,
164
+ "dir": self.dir,
165
+ "zip": self.zip,
166
+ "url_format": self.url_format,
167
+ "current": self._current,
168
+ "latest": self._latest,
169
+ }
170
+ )