circup 2.2.6__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {circup-2.2.6/circup.egg-info → circup-2.4.0}/PKG-INFO +1 -1
- {circup-2.2.6 → circup-2.4.0}/circup/backends.py +5 -7
- circup-2.4.0/circup/bundle.py +280 -0
- {circup-2.2.6 → circup-2.4.0}/circup/command_utils.py +260 -67
- {circup-2.2.6 → circup-2.4.0}/circup/commands.py +175 -39
- {circup-2.2.6 → circup-2.4.0}/circup/module.py +1 -9
- {circup-2.2.6 → circup-2.4.0}/circup/shared.py +4 -25
- {circup-2.2.6 → circup-2.4.0/circup.egg-info}/PKG-INFO +1 -1
- {circup-2.2.6 → circup-2.4.0}/tests/test_circup.py +203 -34
- circup-2.2.6/circup/bundle.py +0 -170
- {circup-2.2.6 → circup-2.4.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {circup-2.2.6 → circup-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {circup-2.2.6 → circup-2.4.0}/.github/workflows/build.yml +0 -0
- {circup-2.2.6 → circup-2.4.0}/.github/workflows/release.yml +0 -0
- {circup-2.2.6 → circup-2.4.0}/.gitignore +0 -0
- {circup-2.2.6 → circup-2.4.0}/.isort.cfg +0 -0
- {circup-2.2.6 → circup-2.4.0}/.pre-commit-config.yaml +0 -0
- {circup-2.2.6 → circup-2.4.0}/.pylintrc +0 -0
- {circup-2.2.6 → circup-2.4.0}/CODE_OF_CONDUCT.rst +0 -0
- {circup-2.2.6 → circup-2.4.0}/CODE_OF_CONDUCT.rst.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/CONTRIBUTING.rst +0 -0
- {circup-2.2.6 → circup-2.4.0}/CONTRIBUTING.rst.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/LICENSE +0 -0
- {circup-2.2.6 → circup-2.4.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/LICENSES/MIT.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/LICENSES/Unlicense.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/README.rst +0 -0
- {circup-2.2.6 → circup-2.4.0}/README.rst.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/__init__.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/config/bundle_config.json +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/config/bundle_config.json.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/lazy_metadata.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/logging.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/wwshell/README.rst +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/wwshell/README.rst.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/wwshell/__init__.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup/wwshell/commands.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup.egg-info/SOURCES.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup.egg-info/dependency_links.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup.egg-info/entry_points.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup.egg-info/requires.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/circup.egg-info/top_level.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/_static/favicon.ico +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/_static/favicon.ico.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/conf.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/index.rst +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/index.rst.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/logo.png +0 -0
- {circup-2.2.6 → circup-2.4.0}/docs/logo.png.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/optional_requirements.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/optional_requirements.txt.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/pyproject.toml +0 -0
- {circup-2.2.6 → circup-2.4.0}/readthedocs.yml +0 -0
- {circup-2.2.6 → circup-2.4.0}/requirements.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/requirements.txt.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/setup.cfg +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/__init__.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/bad_module/__init__.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/bad_module/my_module.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/bad_python.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/bundle.json +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/bundle.json.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/device.json +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/device.json.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/dir_module/__init__.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/dir_module/my_module.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/import_styles.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/local_module.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/local_module_cp7.mpy +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/local_module_cp7.mpy.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/apps/test_app/import_styles.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/apps/test_app/import_styles_sub.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/boot_out.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/boot_out.txt.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/import_styles.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/import_styles_sub.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/.gitignore +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/boot_out.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/boot_out.txt.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/code.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/import_styles_sub.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/package/__init__.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/package/other.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mount_exists.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mount_exists.txt.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mount_missing.txt +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/mount_missing.txt.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/remote_module.py +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config.json +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config.json.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config_local.json +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config_local.json.license +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/test_module.mpy +0 -0
- {circup-2.2.6 → circup-2.4.0}/tests/test_module.mpy.license +0 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -0,0 +1,280 @@
|
|
|
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 semver import VersionInfo
|
|
14
|
+
|
|
15
|
+
from circup.shared import (
|
|
16
|
+
DATA_DIR,
|
|
17
|
+
PLATFORMS,
|
|
18
|
+
REQUESTS_TIMEOUT,
|
|
19
|
+
get_latest_release_from_url,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from circup.logging import logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Bundle: # pylint: disable=too-many-instance-attributes
|
|
26
|
+
"""
|
|
27
|
+
All the links and file names for a bundle
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
#: Avoid requests to the internet
|
|
31
|
+
offline = False
|
|
32
|
+
|
|
33
|
+
def __init__(self, repo):
|
|
34
|
+
"""
|
|
35
|
+
Initialise a Bundle created from its github info.
|
|
36
|
+
Construct all the strings in one place.
|
|
37
|
+
|
|
38
|
+
:param str repo: Repository string for github: "user/repository"
|
|
39
|
+
"""
|
|
40
|
+
vendor, bundle_id = repo.split("/")
|
|
41
|
+
bundle_id = bundle_id.lower().replace("_", "-")
|
|
42
|
+
self.key = repo
|
|
43
|
+
#
|
|
44
|
+
self.url = "https://github.com/" + repo
|
|
45
|
+
self.basename = bundle_id + "-{platform}-{tag}"
|
|
46
|
+
self.urlzip = self.basename + ".zip"
|
|
47
|
+
self.dir = os.path.join(DATA_DIR, vendor, bundle_id + "-{platform}")
|
|
48
|
+
self.zip = os.path.join(DATA_DIR, bundle_id + "-{platform}.zip")
|
|
49
|
+
self.url_format = self.url + "/releases/download/{tag}/" + self.urlzip
|
|
50
|
+
# tag
|
|
51
|
+
self._current = None
|
|
52
|
+
self._latest = None
|
|
53
|
+
self.pinned_tag = None
|
|
54
|
+
self._available = []
|
|
55
|
+
#
|
|
56
|
+
self.platform = None
|
|
57
|
+
|
|
58
|
+
def lib_dir(self, source=False):
|
|
59
|
+
"""
|
|
60
|
+
This bundle's lib directory for the bundle's source or compiled version.
|
|
61
|
+
|
|
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.
|
|
67
|
+
"""
|
|
68
|
+
tag = self.current_tag
|
|
69
|
+
platform = "py" if source or not self.platform else self.platform
|
|
70
|
+
return os.path.join(
|
|
71
|
+
self.dir.format(platform=platform),
|
|
72
|
+
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
73
|
+
"lib",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def examples_dir(self, source=False):
|
|
77
|
+
"""
|
|
78
|
+
This bundle's examples directory for the bundle's source or compiled
|
|
79
|
+
version.
|
|
80
|
+
|
|
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.
|
|
86
|
+
"""
|
|
87
|
+
tag = self.current_tag
|
|
88
|
+
platform = "py" if source or not self.platform else self.platform
|
|
89
|
+
return os.path.join(
|
|
90
|
+
self.dir.format(platform=platform),
|
|
91
|
+
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
92
|
+
"examples",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def requirements_for(self, library_name, toml_file=False):
|
|
96
|
+
"""
|
|
97
|
+
The requirements file for this library.
|
|
98
|
+
|
|
99
|
+
:param str library_name: The name of the library.
|
|
100
|
+
:return: The path to the requirements.txt file.
|
|
101
|
+
"""
|
|
102
|
+
platform = "py"
|
|
103
|
+
tag = self.current_tag
|
|
104
|
+
found_file = os.path.join(
|
|
105
|
+
self.dir.format(platform=platform),
|
|
106
|
+
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
107
|
+
"requirements",
|
|
108
|
+
library_name,
|
|
109
|
+
"requirements.txt" if not toml_file else "pyproject.toml",
|
|
110
|
+
)
|
|
111
|
+
if os.path.isfile(found_file):
|
|
112
|
+
with open(found_file, "r", encoding="utf-8") as read_this:
|
|
113
|
+
return read_this.read()
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def current_tag(self):
|
|
118
|
+
"""
|
|
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.
|
|
123
|
+
|
|
124
|
+
:return: The current tag value for the project.
|
|
125
|
+
"""
|
|
126
|
+
if self._current is None:
|
|
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
|
+
|
|
139
|
+
return self._current
|
|
140
|
+
|
|
141
|
+
@current_tag.setter
|
|
142
|
+
def current_tag(self, tag):
|
|
143
|
+
"""
|
|
144
|
+
Set the current tag (after updating).
|
|
145
|
+
|
|
146
|
+
:param str tag: The new value for the current tag.
|
|
147
|
+
"""
|
|
148
|
+
self._current = tag
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def latest_tag(self):
|
|
152
|
+
"""
|
|
153
|
+
Lazy find the value of the latest tag for the bundle.
|
|
154
|
+
|
|
155
|
+
:return: The most recent tag value for the project.
|
|
156
|
+
"""
|
|
157
|
+
if self._latest is None:
|
|
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
|
+
)
|
|
164
|
+
return self._latest
|
|
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
|
+
|
|
226
|
+
def validate(self):
|
|
227
|
+
"""
|
|
228
|
+
Test the existence of the expected URL (not the content)
|
|
229
|
+
"""
|
|
230
|
+
tag = self.latest_tag
|
|
231
|
+
if not tag or tag == "releases":
|
|
232
|
+
if "--verbose" in sys.argv:
|
|
233
|
+
click.secho(f' Invalid tag "{tag}"', fg="red")
|
|
234
|
+
return False
|
|
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
|
|
243
|
+
return True
|
|
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
|
+
|
|
261
|
+
def __repr__(self):
|
|
262
|
+
"""
|
|
263
|
+
Helps with log files.
|
|
264
|
+
|
|
265
|
+
:return: A repr of a dictionary containing the Bundles's metadata.
|
|
266
|
+
"""
|
|
267
|
+
return repr(
|
|
268
|
+
{
|
|
269
|
+
"key": self.key,
|
|
270
|
+
"url": self.url,
|
|
271
|
+
"urlzip": self.urlzip,
|
|
272
|
+
"dir": self.dir,
|
|
273
|
+
"zip": self.zip,
|
|
274
|
+
"url_format": self.url_format,
|
|
275
|
+
"current": self._current,
|
|
276
|
+
"latest": self._latest,
|
|
277
|
+
"pinned": self.pinned_tag,
|
|
278
|
+
"available": self._available,
|
|
279
|
+
}
|
|
280
|
+
)
|