circup 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- circup/__init__.py +26 -0
- circup/backends.py +957 -0
- circup/bundle.py +170 -0
- circup/command_utils.py +627 -0
- circup/commands.py +723 -0
- circup/config/bundle_config.json +5 -0
- circup/config/bundle_config.json.license +3 -0
- circup/logging.py +33 -0
- circup/module.py +209 -0
- circup/shared.py +218 -0
- circup-2.0.1.dist-info/LICENSE +21 -0
- circup-2.0.1.dist-info/METADATA +357 -0
- circup-2.0.1.dist-info/RECORD +16 -0
- circup-2.0.1.dist-info/WHEEL +5 -0
- circup-2.0.1.dist-info/entry_points.txt +2 -0
- circup-2.0.1.dist-info/top_level.txt +1 -0
circup/logging.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""
|
|
5
|
+
Logging utilities and configuration used by circup
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
from logging.handlers import RotatingFileHandler
|
|
10
|
+
import appdirs
|
|
11
|
+
|
|
12
|
+
from circup.shared import DATA_DIR
|
|
13
|
+
|
|
14
|
+
#: The directory containing the utility's log file.
|
|
15
|
+
LOG_DIR = appdirs.user_log_dir(appname="circup", appauthor="adafruit")
|
|
16
|
+
#: The location of the log file for the utility.
|
|
17
|
+
LOGFILE = os.path.join(LOG_DIR, "circup.log")
|
|
18
|
+
|
|
19
|
+
# Ensure DATA_DIR / LOG_DIR related directories and files exist.
|
|
20
|
+
if not os.path.exists(DATA_DIR): # pragma: no cover
|
|
21
|
+
os.makedirs(DATA_DIR)
|
|
22
|
+
if not os.path.exists(LOG_DIR): # pragma: no cover
|
|
23
|
+
os.makedirs(LOG_DIR)
|
|
24
|
+
|
|
25
|
+
# Setup logging.
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
logger.setLevel(logging.INFO)
|
|
28
|
+
logfile_handler = RotatingFileHandler(LOGFILE, maxBytes=10_000_000, backupCount=0)
|
|
29
|
+
log_formatter = logging.Formatter(
|
|
30
|
+
"%(asctime)s %(levelname)s: %(message)s", datefmt="%m/%d/%Y %H:%M:%S"
|
|
31
|
+
)
|
|
32
|
+
logfile_handler.setFormatter(log_formatter)
|
|
33
|
+
logger.addHandler(logfile_handler)
|
circup/module.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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 CircuitPython module on a device or in a Bundle.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from urllib.parse import urljoin, urlparse
|
|
9
|
+
from semver import VersionInfo
|
|
10
|
+
|
|
11
|
+
from circup.shared import BAD_FILE_FORMAT
|
|
12
|
+
from circup.backends import WebBackend
|
|
13
|
+
from circup.logging import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Module:
|
|
17
|
+
"""
|
|
18
|
+
Represents a CircuitPython module.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# pylint: disable=too-many-arguments
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
name,
|
|
26
|
+
backend,
|
|
27
|
+
repo,
|
|
28
|
+
device_version,
|
|
29
|
+
bundle_version,
|
|
30
|
+
mpy,
|
|
31
|
+
bundle,
|
|
32
|
+
compatibility,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
The ``self.file`` and ``self.name`` attributes are constructed from
|
|
36
|
+
the ``path`` value. If the path is to a directory based module, the
|
|
37
|
+
resulting self.file value will be None, and the name will be the
|
|
38
|
+
basename of the directory path.
|
|
39
|
+
|
|
40
|
+
:param str name: The file name of the module.
|
|
41
|
+
:param Backend backend: The backend that the module is on.
|
|
42
|
+
:param str repo: The URL of the Git repository for this module.
|
|
43
|
+
:param str device_version: The semver value for the version on device.
|
|
44
|
+
:param str bundle_version: The semver value for the version in bundle.
|
|
45
|
+
:param bool mpy: Flag to indicate if the module is byte-code compiled.
|
|
46
|
+
:param Bundle bundle: Bundle object where the module is located.
|
|
47
|
+
:param (str,str) compatibility: Min and max versions of CP compatible with the mpy.
|
|
48
|
+
"""
|
|
49
|
+
self.name = name
|
|
50
|
+
self.backend = backend
|
|
51
|
+
self.path = (
|
|
52
|
+
urljoin(backend.library_path, name, allow_fragments=False)
|
|
53
|
+
if isinstance(backend, WebBackend)
|
|
54
|
+
else os.path.join(backend.library_path, name)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
url = urlparse(self.path, allow_fragments=False)
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
url.path.endswith("/")
|
|
61
|
+
if isinstance(backend, WebBackend)
|
|
62
|
+
else self.path.endswith(os.sep)
|
|
63
|
+
):
|
|
64
|
+
self.file = None
|
|
65
|
+
self.name = self.path.split(
|
|
66
|
+
"/" if isinstance(backend, WebBackend) else os.sep
|
|
67
|
+
)[-2]
|
|
68
|
+
else:
|
|
69
|
+
self.file = os.path.basename(url.path)
|
|
70
|
+
self.name = (
|
|
71
|
+
os.path.basename(url.path).replace(".py", "").replace(".mpy", "")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.repo = repo
|
|
75
|
+
self.device_version = device_version
|
|
76
|
+
self.bundle_version = bundle_version
|
|
77
|
+
self.mpy = mpy
|
|
78
|
+
self.min_version = compatibility[0]
|
|
79
|
+
self.max_version = compatibility[1]
|
|
80
|
+
# Figure out the bundle path.
|
|
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
|
+
# module path in the bundle
|
|
91
|
+
search_path = bundle.lib_dir(bundle_platform)
|
|
92
|
+
if self.file:
|
|
93
|
+
self.bundle_path = os.path.join(search_path, self.file)
|
|
94
|
+
else:
|
|
95
|
+
self.bundle_path = os.path.join(search_path, self.name)
|
|
96
|
+
logger.info(self)
|
|
97
|
+
|
|
98
|
+
# pylint: enable=too-many-arguments
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def outofdate(self):
|
|
102
|
+
"""
|
|
103
|
+
Returns a boolean to indicate if this module is out of date.
|
|
104
|
+
Treat mismatched MPY versions as out of date.
|
|
105
|
+
|
|
106
|
+
:return: Truthy indication if the module is out of date.
|
|
107
|
+
"""
|
|
108
|
+
if self.mpy_mismatch:
|
|
109
|
+
return True
|
|
110
|
+
if self.device_version and self.bundle_version:
|
|
111
|
+
try:
|
|
112
|
+
return VersionInfo.parse(self.device_version) < VersionInfo.parse(
|
|
113
|
+
self.bundle_version
|
|
114
|
+
)
|
|
115
|
+
except ValueError as ex:
|
|
116
|
+
logger.warning("Module '%s' has incorrect semver value.", self.name)
|
|
117
|
+
logger.warning(ex)
|
|
118
|
+
return True # Assume out of date to try to update.
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def bad_format(self):
|
|
122
|
+
"""A boolean indicating that the mpy file format could not be identified"""
|
|
123
|
+
return self.mpy and self.device_version == BAD_FILE_FORMAT
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def mpy_mismatch(self):
|
|
127
|
+
"""
|
|
128
|
+
Returns a boolean to indicate if this module's MPY version is compatible
|
|
129
|
+
with the board's current version of Circuitpython. A min or max version
|
|
130
|
+
that evals to False means no limit.
|
|
131
|
+
|
|
132
|
+
:return: Boolean indicating if the MPY versions don't match.
|
|
133
|
+
"""
|
|
134
|
+
if not self.mpy:
|
|
135
|
+
return False
|
|
136
|
+
try:
|
|
137
|
+
cpv = VersionInfo.parse(self.backend.get_circuitpython_version()[0])
|
|
138
|
+
except ValueError as ex:
|
|
139
|
+
logger.warning("CircuitPython has incorrect semver value.")
|
|
140
|
+
logger.warning(ex)
|
|
141
|
+
try:
|
|
142
|
+
if self.min_version and cpv < VersionInfo.parse(self.min_version):
|
|
143
|
+
return True # CP version too old
|
|
144
|
+
if self.max_version and cpv >= VersionInfo.parse(self.max_version):
|
|
145
|
+
return True # MPY version too old
|
|
146
|
+
except (TypeError, ValueError) as ex:
|
|
147
|
+
logger.warning(
|
|
148
|
+
"Module '%s' has incorrect MPY compatibility information.", self.name
|
|
149
|
+
)
|
|
150
|
+
logger.warning(ex)
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def major_update(self):
|
|
155
|
+
"""
|
|
156
|
+
Returns a boolean to indicate if this is a major version update.
|
|
157
|
+
|
|
158
|
+
:return: Boolean indicating if this is a major version upgrade
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
if (
|
|
162
|
+
VersionInfo.parse(self.device_version).major
|
|
163
|
+
== VersionInfo.parse(self.bundle_version).major
|
|
164
|
+
):
|
|
165
|
+
return False
|
|
166
|
+
except (TypeError, ValueError) as ex:
|
|
167
|
+
logger.warning("Module '%s' has incorrect semver value.", self.name)
|
|
168
|
+
logger.warning(ex)
|
|
169
|
+
return True # Assume Major Version udpate.
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def row(self):
|
|
173
|
+
"""
|
|
174
|
+
Returns a tuple of items to display in a table row to show the module's
|
|
175
|
+
name, local version and remote version, and reason to update.
|
|
176
|
+
|
|
177
|
+
:return: A tuple containing the module's name, version on the connected
|
|
178
|
+
device, version in the latest bundle and reason to update.
|
|
179
|
+
"""
|
|
180
|
+
loc = self.device_version if self.device_version else "unknown"
|
|
181
|
+
rem = self.bundle_version if self.bundle_version else "unknown"
|
|
182
|
+
if self.mpy_mismatch:
|
|
183
|
+
update_reason = "MPY Format"
|
|
184
|
+
elif self.major_update:
|
|
185
|
+
update_reason = "Major Version"
|
|
186
|
+
else:
|
|
187
|
+
update_reason = "Minor Version"
|
|
188
|
+
return (self.name, loc, rem, update_reason)
|
|
189
|
+
|
|
190
|
+
def __repr__(self):
|
|
191
|
+
"""
|
|
192
|
+
Helps with log files.
|
|
193
|
+
|
|
194
|
+
:return: A repr of a dictionary containing the module's metadata.
|
|
195
|
+
"""
|
|
196
|
+
return repr(
|
|
197
|
+
{
|
|
198
|
+
"path": self.path,
|
|
199
|
+
"file": self.file,
|
|
200
|
+
"name": self.name,
|
|
201
|
+
"repo": self.repo,
|
|
202
|
+
"device_version": self.device_version,
|
|
203
|
+
"bundle_version": self.bundle_version,
|
|
204
|
+
"bundle_path": self.bundle_path,
|
|
205
|
+
"mpy": self.mpy,
|
|
206
|
+
"min_version": self.min_version,
|
|
207
|
+
"max_version": self.max_version,
|
|
208
|
+
}
|
|
209
|
+
)
|
circup/shared.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
|
2
|
+
# SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
"""
|
|
6
|
+
Utilities that are shared and used by both click CLI command functions
|
|
7
|
+
and Backend class functions.
|
|
8
|
+
"""
|
|
9
|
+
import glob
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import json
|
|
13
|
+
import appdirs
|
|
14
|
+
import pkg_resources
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
#: Version identifier for a bad MPY file format
|
|
18
|
+
BAD_FILE_FORMAT = "Invalid"
|
|
19
|
+
|
|
20
|
+
#: The location of data files used by circup (following OS conventions).
|
|
21
|
+
DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
|
|
22
|
+
|
|
23
|
+
#: Module formats list (and the other form used in github files)
|
|
24
|
+
PLATFORMS = {"py": "py", "8mpy": "8.x-mpy", "9mpy": "9.x-mpy"}
|
|
25
|
+
|
|
26
|
+
#: Timeout for requests calls like get()
|
|
27
|
+
REQUESTS_TIMEOUT = 30
|
|
28
|
+
|
|
29
|
+
#: The path to the JSON file containing the metadata about the bundles.
|
|
30
|
+
BUNDLE_CONFIG_FILE = pkg_resources.resource_filename(
|
|
31
|
+
"circup", "config/bundle_config.json"
|
|
32
|
+
)
|
|
33
|
+
#: Overwrite the bundles list with this file (only done manually)
|
|
34
|
+
BUNDLE_CONFIG_OVERWRITE = os.path.join(DATA_DIR, "bundle_config.json")
|
|
35
|
+
#: The path to the JSON file containing the local list of bundles.
|
|
36
|
+
BUNDLE_CONFIG_LOCAL = os.path.join(DATA_DIR, "bundle_config_local.json")
|
|
37
|
+
#: The path to the JSON file containing the metadata about the bundles.
|
|
38
|
+
BUNDLE_DATA = os.path.join(DATA_DIR, "circup.json")
|
|
39
|
+
|
|
40
|
+
#: The libraries (and blank lines) which don't go on devices
|
|
41
|
+
NOT_MCU_LIBRARIES = [
|
|
42
|
+
"",
|
|
43
|
+
"adafruit-blinka",
|
|
44
|
+
"adafruit-blinka-bleio",
|
|
45
|
+
"adafruit-blinka-displayio",
|
|
46
|
+
"adafruit-circuitpython-typing",
|
|
47
|
+
"circuitpython_typing",
|
|
48
|
+
"pyserial",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
#: Commands that do not require an attached board
|
|
52
|
+
BOARDLESS_COMMANDS = ["show", "bundle-add", "bundle-remove", "bundle-show"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_modules_file(path, logger):
|
|
56
|
+
"""
|
|
57
|
+
Get a dictionary containing metadata about all the Python modules found in
|
|
58
|
+
the referenced file system path.
|
|
59
|
+
|
|
60
|
+
:param str path: The directory in which to find modules.
|
|
61
|
+
:return: A dictionary containing metadata about the found modules.
|
|
62
|
+
"""
|
|
63
|
+
result = {}
|
|
64
|
+
if not path:
|
|
65
|
+
return result
|
|
66
|
+
single_file_py_mods = glob.glob(os.path.join(path, "*.py"))
|
|
67
|
+
single_file_mpy_mods = glob.glob(os.path.join(path, "*.mpy"))
|
|
68
|
+
package_dir_mods = [
|
|
69
|
+
d
|
|
70
|
+
for d in glob.glob(os.path.join(path, "*", ""))
|
|
71
|
+
if not os.path.basename(os.path.normpath(d)).startswith(".")
|
|
72
|
+
]
|
|
73
|
+
single_file_mods = single_file_py_mods + single_file_mpy_mods
|
|
74
|
+
for sfm in [f for f in single_file_mods if not os.path.basename(f).startswith(".")]:
|
|
75
|
+
metadata = extract_metadata(sfm, logger)
|
|
76
|
+
metadata["path"] = sfm
|
|
77
|
+
result[os.path.basename(sfm).replace(".py", "").replace(".mpy", "")] = metadata
|
|
78
|
+
for package_path in package_dir_mods:
|
|
79
|
+
name = os.path.basename(os.path.dirname(package_path))
|
|
80
|
+
py_files = glob.glob(os.path.join(package_path, "**/*.py"), recursive=True)
|
|
81
|
+
mpy_files = glob.glob(os.path.join(package_path, "**/*.mpy"), recursive=True)
|
|
82
|
+
all_files = py_files + mpy_files
|
|
83
|
+
# default value
|
|
84
|
+
result[name] = {"path": package_path, "mpy": bool(mpy_files)}
|
|
85
|
+
# explore all the submodules to detect bad ones
|
|
86
|
+
for source in [f for f in all_files if not os.path.basename(f).startswith(".")]:
|
|
87
|
+
metadata = extract_metadata(source, logger)
|
|
88
|
+
if "__version__" in metadata:
|
|
89
|
+
metadata["path"] = package_path
|
|
90
|
+
result[name] = metadata
|
|
91
|
+
# break now if any of the submodules has a bad format
|
|
92
|
+
if metadata["__version__"] == BAD_FILE_FORMAT:
|
|
93
|
+
break
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_metadata(path, logger):
|
|
98
|
+
# pylint: disable=too-many-locals,too-many-branches
|
|
99
|
+
"""
|
|
100
|
+
Given a file path, return a dictionary containing metadata extracted from
|
|
101
|
+
dunder attributes found therein. Works with both .py and .mpy files.
|
|
102
|
+
|
|
103
|
+
For Python source files, such metadata assignments should be simple and
|
|
104
|
+
single-line. For example::
|
|
105
|
+
|
|
106
|
+
__version__ = "1.1.4"
|
|
107
|
+
__repo__ = "https://github.com/adafruit/SomeLibrary.git"
|
|
108
|
+
|
|
109
|
+
For byte compiled .mpy files, a brute force / backtrack approach is used
|
|
110
|
+
to find the __version__ number in the file -- see comments in the
|
|
111
|
+
code for the implementation details.
|
|
112
|
+
|
|
113
|
+
:param str path: The path to the file containing the metadata.
|
|
114
|
+
:return: The dunder based metadata found in the file, as a dictionary.
|
|
115
|
+
"""
|
|
116
|
+
result = {}
|
|
117
|
+
logger.info("%s", path)
|
|
118
|
+
if path.endswith(".py"):
|
|
119
|
+
result["mpy"] = False
|
|
120
|
+
with open(path, "r", encoding="utf-8") as source_file:
|
|
121
|
+
content = source_file.read()
|
|
122
|
+
#: The regex used to extract ``__version__`` and ``__repo__`` assignments.
|
|
123
|
+
dunder_key_val = r"""(__\w+__)(?:\s*:\s*\w+)?\s*=\s*(?:['"]|\(\s)(.+)['"]"""
|
|
124
|
+
for match in re.findall(dunder_key_val, content):
|
|
125
|
+
result[match[0]] = str(match[1])
|
|
126
|
+
if result:
|
|
127
|
+
logger.info("Extracted metadata: %s", result)
|
|
128
|
+
elif path.endswith(".mpy"):
|
|
129
|
+
find_by_regexp_match = False
|
|
130
|
+
result["mpy"] = True
|
|
131
|
+
with open(path, "rb") as mpy_file:
|
|
132
|
+
content = mpy_file.read()
|
|
133
|
+
# Track the MPY version number
|
|
134
|
+
mpy_version = content[0:2]
|
|
135
|
+
compatibility = None
|
|
136
|
+
loc = -1
|
|
137
|
+
# Find the start location of the __version__
|
|
138
|
+
if mpy_version == b"M\x03":
|
|
139
|
+
# One byte for the length of "__version__"
|
|
140
|
+
loc = content.find(b"__version__") - 1
|
|
141
|
+
compatibility = (None, "7.0.0-alpha.1")
|
|
142
|
+
elif mpy_version == b"C\x05":
|
|
143
|
+
# Two bytes for the length of "__version__" in mpy version 5
|
|
144
|
+
loc = content.find(b"__version__") - 2
|
|
145
|
+
compatibility = ("7.0.0-alpha.1", "8.99.99")
|
|
146
|
+
elif mpy_version == b"C\x06":
|
|
147
|
+
# Two bytes in mpy version 6
|
|
148
|
+
find_by_regexp_match = True
|
|
149
|
+
compatibility = ("9.0.0-alpha.1", None)
|
|
150
|
+
if find_by_regexp_match:
|
|
151
|
+
# Too hard to find the version positionally.
|
|
152
|
+
# Find the first thing that looks like an x.y.z version number.
|
|
153
|
+
match = re.search(rb"([\d]+\.[\d]+\.[\d]+)\x00", content)
|
|
154
|
+
if match:
|
|
155
|
+
result["__version__"] = match.group(1).decode("utf-8")
|
|
156
|
+
elif loc > -1:
|
|
157
|
+
# Backtrack until a byte value of the offset is reached.
|
|
158
|
+
offset = 1
|
|
159
|
+
while offset < loc:
|
|
160
|
+
val = int(content[loc - offset])
|
|
161
|
+
if mpy_version == b"C\x05":
|
|
162
|
+
val = val // 2
|
|
163
|
+
if val == offset - 1: # Off by one..!
|
|
164
|
+
# Found version, extract the number given boundaries.
|
|
165
|
+
start = loc - offset + 1 # No need for prepended length.
|
|
166
|
+
end = loc # Up to the start of the __version__.
|
|
167
|
+
version = content[start:end] # Slice the version number.
|
|
168
|
+
# Create a string version as metadata in the result.
|
|
169
|
+
result["__version__"] = version.decode("utf-8")
|
|
170
|
+
break # Nothing more to do.
|
|
171
|
+
offset += 1 # ...and again but backtrack by one.
|
|
172
|
+
if compatibility:
|
|
173
|
+
result["compatibility"] = compatibility
|
|
174
|
+
else:
|
|
175
|
+
# not a valid MPY file
|
|
176
|
+
result["__version__"] = BAD_FILE_FORMAT
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def tags_data_load(logger):
|
|
181
|
+
"""
|
|
182
|
+
Load the list of the version tags of the bundles on disk.
|
|
183
|
+
|
|
184
|
+
:return: a dict() of tags indexed by Bundle identifiers/keys.
|
|
185
|
+
"""
|
|
186
|
+
tags_data = None
|
|
187
|
+
try:
|
|
188
|
+
with open(BUNDLE_DATA, encoding="utf-8") as data:
|
|
189
|
+
try:
|
|
190
|
+
tags_data = json.load(data)
|
|
191
|
+
except json.decoder.JSONDecodeError as ex:
|
|
192
|
+
# Sometimes (why?) the JSON file becomes corrupt. In which case
|
|
193
|
+
# log it and carry on as if setting up for first time.
|
|
194
|
+
logger.error("Could not parse %s", BUNDLE_DATA)
|
|
195
|
+
logger.exception(ex)
|
|
196
|
+
except FileNotFoundError:
|
|
197
|
+
pass
|
|
198
|
+
if not isinstance(tags_data, dict):
|
|
199
|
+
tags_data = {}
|
|
200
|
+
return tags_data
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_latest_release_from_url(url, logger):
|
|
204
|
+
"""
|
|
205
|
+
Find the tag name of the latest release by using HTTP HEAD and decoding the redirect.
|
|
206
|
+
|
|
207
|
+
:param str url: URL to the latest release page on a git repository.
|
|
208
|
+
:return: The most recent tag value for the release.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
logger.info("Requesting redirect information: %s", url)
|
|
212
|
+
response = requests.head(url, timeout=REQUESTS_TIMEOUT)
|
|
213
|
+
responseurl = response.url
|
|
214
|
+
if response.is_redirect:
|
|
215
|
+
responseurl = response.headers["Location"]
|
|
216
|
+
tag = responseurl.rsplit("/", 1)[-1]
|
|
217
|
+
logger.info("Tag: '%s'", tag)
|
|
218
|
+
return tag
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Adafruit Industries
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|