snap-python 0.0.4__tar.gz → 0.1.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.
- {snap_python-0.0.4 → snap_python-0.1.0}/PKG-INFO +12 -3
- {snap_python-0.0.4 → snap_python-0.1.0}/README.md +9 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/pyproject.toml +7 -1
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/client.py +37 -6
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/components/snaps.py +113 -13
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/components/store.py +28 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/snaps.py +7 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/search.py +33 -6
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/utils.py +6 -2
- {snap_python-0.0.4 → snap_python-0.1.0}/LICENSE +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/__init__.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/components/__init__.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/components/config.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/changes.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/common.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/config.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/__init__.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/categories.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/info.py +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/category_by_name_schema.json +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/category_schema.json +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/snaps_find_schema.json +0 -0
- {snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/snaps_info_schema.json +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: snap-python
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: An api to interact with snapd and snap stores
|
|
5
|
-
Home-page: https://github.com/alexlukens/snap-python
|
|
6
5
|
License: MIT
|
|
7
6
|
Author: Alexander Lukens
|
|
8
7
|
Author-email: alexdlukens@gmail.com
|
|
@@ -12,6 +11,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
16
16
|
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
17
17
|
Requires-Dist: retry (>=0.9.2,<0.10.0)
|
|
@@ -38,6 +38,15 @@ Here is a simple example of how to use snap_python:
|
|
|
38
38
|
# TODO
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Create a Release
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git tag -a v0.x.x -m "Release v0.x.x"
|
|
45
|
+
gh release create v0.x.x --title "Release v0.x.x"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The automation will build a wheel and push it to pypi
|
|
49
|
+
|
|
41
50
|
## License
|
|
42
51
|
|
|
43
52
|
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
@@ -18,6 +18,15 @@ Here is a simple example of how to use snap_python:
|
|
|
18
18
|
# TODO
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Create a Release
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git tag -a v0.x.x -m "Release v0.x.x"
|
|
25
|
+
gh release create v0.x.x --title "Release v0.x.x"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The automation will build a wheel and push it to pypi
|
|
29
|
+
|
|
21
30
|
## License
|
|
22
31
|
|
|
23
32
|
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "snap-python"
|
|
3
|
-
version = "0.0
|
|
3
|
+
version = "0.1.0"
|
|
4
4
|
description = "An api to interact with snapd and snap stores"
|
|
5
5
|
authors = ["Alexander Lukens <alexdlukens@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -27,3 +27,9 @@ sphinx-rtd-theme = "^3.0.2"
|
|
|
27
27
|
[build-system]
|
|
28
28
|
requires = ["poetry-core"]
|
|
29
29
|
build-backend = "poetry.core.masonry.api"
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
asyncio_mode = "auto"
|
|
33
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
34
|
+
addopts = "--cov=snap_python --cov-report=html"
|
|
35
|
+
testpaths = "tests"
|
|
@@ -27,6 +27,7 @@ class SnapClient(AbstractSnapsClient):
|
|
|
27
27
|
tcp_location: str = None,
|
|
28
28
|
store_base_url: str = "https://api.snapcraft.io",
|
|
29
29
|
store_headers: dict[str, str] = None,
|
|
30
|
+
prompt_for_authentication: bool = False,
|
|
30
31
|
):
|
|
31
32
|
if tcp_location and snapd_socket_location:
|
|
32
33
|
raise ValueError(
|
|
@@ -40,14 +41,19 @@ class SnapClient(AbstractSnapsClient):
|
|
|
40
41
|
self._transport = httpx.AsyncHTTPTransport(
|
|
41
42
|
uds=snapd_socket_location or SNAPD_SOCKET
|
|
42
43
|
)
|
|
43
|
-
|
|
44
44
|
if store_headers is None:
|
|
45
45
|
store_headers = {"Snap-Device-Series": "16", "X-Ubuntu-Series": "16"}
|
|
46
46
|
|
|
47
|
+
self.snapd_headers = {}
|
|
48
|
+
if prompt_for_authentication:
|
|
49
|
+
self.snapd_headers = {"X-Allow-Interaction": "true"}
|
|
50
|
+
|
|
47
51
|
self.version = version
|
|
48
52
|
self.store_base_url = store_base_url
|
|
49
53
|
self.store_headers = store_headers
|
|
50
|
-
self.snapd_client = httpx.AsyncClient(
|
|
54
|
+
self.snapd_client = httpx.AsyncClient(
|
|
55
|
+
transport=self._transport, headers=self.snapd_headers
|
|
56
|
+
)
|
|
51
57
|
self.snaps = SnapsEndpoints(self)
|
|
52
58
|
self.store = StoreEndpoints(
|
|
53
59
|
base_url=self.store_base_url,
|
|
@@ -55,6 +61,7 @@ class SnapClient(AbstractSnapsClient):
|
|
|
55
61
|
headers=self.store_headers,
|
|
56
62
|
)
|
|
57
63
|
self.config = ConfigEndpoints(self)
|
|
64
|
+
self.snapd_client = httpx.AsyncClient(transport=self._transport)
|
|
58
65
|
|
|
59
66
|
@retry(httpx.HTTPError, tries=3, delay=1, backoff=1)
|
|
60
67
|
async def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
@@ -105,30 +112,54 @@ class SnapClient(AbstractSnapsClient):
|
|
|
105
112
|
|
|
106
113
|
return response
|
|
107
114
|
|
|
115
|
+
@retry(httpx.HTTPError, tries=3, delay=0.5)
|
|
108
116
|
async def get_changes_by_id(self, change_id: str) -> ChangesResponse:
|
|
117
|
+
"""
|
|
118
|
+
Asynchronously retrieves changes by their ID.
|
|
119
|
+
Args:
|
|
120
|
+
change_id (str): The ID of the change to retrieve.
|
|
121
|
+
Returns:
|
|
122
|
+
ChangesResponse: The response object containing the changes.
|
|
123
|
+
Raises:
|
|
124
|
+
httpx.HTTPStatusError: If the request results in an HTTP error.
|
|
125
|
+
"""
|
|
126
|
+
|
|
109
127
|
try:
|
|
110
128
|
response = await self.request("GET", f"changes/{change_id}")
|
|
111
129
|
except httpx.HTTPStatusError as e:
|
|
112
130
|
logger.warning(
|
|
113
|
-
|
|
131
|
+
"Bad response status code for get_changes_by_id: %s",
|
|
132
|
+
e.response.status_code,
|
|
114
133
|
)
|
|
115
134
|
response = e.response
|
|
116
|
-
pass
|
|
117
135
|
|
|
118
136
|
return ChangesResponse.model_validate_json(response.content)
|
|
119
137
|
|
|
120
138
|
async def get_changes_by_id_generator(
|
|
121
139
|
self, change_id: str
|
|
122
140
|
) -> AsyncGenerator[ChangesResponse, None]:
|
|
141
|
+
"""
|
|
142
|
+
Asynchronous generator to fetch changes by change ID.
|
|
143
|
+
This generator continuously requests changes from the server using the provided
|
|
144
|
+
change ID and yields the response as a ChangesResponse object. It includes error
|
|
145
|
+
handling for HTTP status errors and adds a small delay between requests to avoid
|
|
146
|
+
overwhelming the server.
|
|
147
|
+
Args:
|
|
148
|
+
change_id (str): The ID of the change to fetch.
|
|
149
|
+
Yields:
|
|
150
|
+
ChangesResponse: The response object containing the changes.
|
|
151
|
+
Raises:
|
|
152
|
+
httpx.HTTPStatusError: If the request fails with an HTTP status error.
|
|
153
|
+
"""
|
|
123
154
|
while True:
|
|
124
155
|
try:
|
|
125
156
|
response = await self.request("GET", f"changes/{change_id}")
|
|
126
157
|
except httpx.HTTPStatusError as e:
|
|
127
158
|
logger.warning(
|
|
128
|
-
|
|
159
|
+
"Bad response status code for get_changes_by_id: %s",
|
|
160
|
+
e.response.status_code,
|
|
129
161
|
)
|
|
130
162
|
response = e.response
|
|
131
|
-
pass
|
|
132
163
|
|
|
133
164
|
change_response = ChangesResponse.model_validate_json(response.content)
|
|
134
165
|
yield change_response
|
|
@@ -10,7 +10,7 @@ from snap_python.schemas.snaps import (
|
|
|
10
10
|
InstalledSnapListResponse,
|
|
11
11
|
SingleInstalledSnapResponse,
|
|
12
12
|
)
|
|
13
|
-
from snap_python.utils import AbstractSnapsClient, going_to_reload_daemon
|
|
13
|
+
from snap_python.utils import AbstractSnapsClient, SnapdAPIError, going_to_reload_daemon
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger("snap_python.components.snaps")
|
|
16
16
|
|
|
@@ -60,8 +60,8 @@ class SnapsEndpoints:
|
|
|
60
60
|
snap: str,
|
|
61
61
|
channel: str = "stable",
|
|
62
62
|
classic: bool = False,
|
|
63
|
+
dangerous: bool = False,
|
|
63
64
|
devmode: bool = False,
|
|
64
|
-
ignore_validation: bool = False,
|
|
65
65
|
jailmode: bool = False,
|
|
66
66
|
revision: int = None,
|
|
67
67
|
filename: str = None,
|
|
@@ -76,15 +76,15 @@ class SnapsEndpoints:
|
|
|
76
76
|
snap (str): name of the snap to install
|
|
77
77
|
channel (str, optional): Channel to install. Defaults to "stable".
|
|
78
78
|
classic (bool, optional): Install with classic confinement. Defaults to False.
|
|
79
|
+
dangerous (bool, optional): install the given snap files even if there are no pre-acknowledged signatures for them, meaning they are not verified and could be dangerous if true (optional, implied by devmode). Defaults to False.
|
|
79
80
|
devmode (bool, optional): Install with devmode. Defaults to False.
|
|
80
|
-
ignore_validation (bool, optional): _description_. Defaults to False.
|
|
81
81
|
jailmode (bool, optional): Install snap with jailmode. Defaults to False.
|
|
82
82
|
revision (int, optional): install a specific revision of the snap. Defaults to None.
|
|
83
83
|
filename (str, optional): Path to snap to sideload. Defaults to None.
|
|
84
84
|
wait (bool, optional): Whether to wait for snap to install. If not waiting, will return async response with change id. Defaults to False.
|
|
85
85
|
|
|
86
86
|
Raises:
|
|
87
|
-
|
|
87
|
+
SnapdAPIError: If error occurs during snap install
|
|
88
88
|
|
|
89
89
|
Returns:
|
|
90
90
|
AsyncResponse | ChangesResponse: If wait is True, will return ChangesResponse. Otherwise, will return AsyncResponse
|
|
@@ -93,8 +93,8 @@ class SnapsEndpoints:
|
|
|
93
93
|
"action": "install",
|
|
94
94
|
"channel": channel,
|
|
95
95
|
"classic": classic,
|
|
96
|
+
"dangerous": dangerous,
|
|
96
97
|
"devmode": devmode,
|
|
97
|
-
"ignore_validation": ignore_validation,
|
|
98
98
|
"jailmode": jailmode,
|
|
99
99
|
}
|
|
100
100
|
if revision:
|
|
@@ -103,7 +103,10 @@ class SnapsEndpoints:
|
|
|
103
103
|
# sideload
|
|
104
104
|
if not Path(filename).exists():
|
|
105
105
|
raise FileNotFoundError(f"File {filename} does not exist")
|
|
106
|
-
request_data
|
|
106
|
+
if request_data.get("dangerous") is not True:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"Cannot sideload snap without dangerous flag set to True"
|
|
109
|
+
)
|
|
107
110
|
raw_response: httpx.Response = await self._client.request(
|
|
108
111
|
"POST",
|
|
109
112
|
f"{self.common_endpoint}",
|
|
@@ -122,17 +125,16 @@ class SnapsEndpoints:
|
|
|
122
125
|
while True:
|
|
123
126
|
try:
|
|
124
127
|
changes = await self._client.get_changes_by_id(changes_id)
|
|
125
|
-
logger.debug(
|
|
126
|
-
except httpx.HTTPError
|
|
128
|
+
logger.debug("Progress: %s", changes.result.overall_progress)
|
|
129
|
+
except httpx.HTTPError:
|
|
127
130
|
if going_to_reload_daemon(previous_changes):
|
|
128
131
|
logger.debug("Waiting for daemon to reload")
|
|
129
|
-
|
|
130
|
-
continue
|
|
132
|
+
changes = previous_changes
|
|
131
133
|
|
|
132
134
|
if changes.ready:
|
|
133
135
|
break
|
|
134
136
|
if changes.result.err:
|
|
135
|
-
raise
|
|
137
|
+
raise SnapdAPIError(f"Error in snap install: {changes.result.err}")
|
|
136
138
|
await asyncio.sleep(0.1)
|
|
137
139
|
previous_changes = changes
|
|
138
140
|
return changes
|
|
@@ -145,6 +147,18 @@ class SnapsEndpoints:
|
|
|
145
147
|
terminate: bool = False,
|
|
146
148
|
wait: bool = False,
|
|
147
149
|
) -> AsyncResponse | ChangesResponse:
|
|
150
|
+
"""
|
|
151
|
+
Asynchronously removes a snap package.
|
|
152
|
+
Args:
|
|
153
|
+
snap (str): The name of the snap package to remove.
|
|
154
|
+
purge (bool, optional): If True, purges the snap package. Defaults to False.
|
|
155
|
+
terminate (bool, optional): If True, terminates the snap package. Defaults to False.
|
|
156
|
+
wait (bool, optional): If True, waits for the removal process to complete. Defaults to False.
|
|
157
|
+
Returns:
|
|
158
|
+
AsyncResponse | ChangesResponse: The response from the snapd API, either an asynchronous response or a changes response if waiting for completion.
|
|
159
|
+
Raises:
|
|
160
|
+
SnapdAPIError: If there is an error in the snap removal process.
|
|
161
|
+
"""
|
|
148
162
|
request_data = {
|
|
149
163
|
"action": "remove",
|
|
150
164
|
"purge": purge,
|
|
@@ -162,7 +176,7 @@ class SnapsEndpoints:
|
|
|
162
176
|
while True:
|
|
163
177
|
try:
|
|
164
178
|
changes = await self._client.get_changes_by_id(changes_id)
|
|
165
|
-
except httpx.HTTPError
|
|
179
|
+
except httpx.HTTPError:
|
|
166
180
|
if going_to_reload_daemon(previous_changes):
|
|
167
181
|
logger.debug("Waiting for daemon to reload")
|
|
168
182
|
await asyncio.sleep(0.1)
|
|
@@ -170,9 +184,95 @@ class SnapsEndpoints:
|
|
|
170
184
|
if changes.ready:
|
|
171
185
|
break
|
|
172
186
|
if changes.result.err:
|
|
173
|
-
raise
|
|
187
|
+
raise SnapdAPIError(f"Error in snap remove: {changes.result.err}")
|
|
174
188
|
await asyncio.sleep(0.1)
|
|
175
189
|
previous_changes = changes
|
|
176
190
|
return changes
|
|
177
191
|
|
|
178
192
|
return response
|
|
193
|
+
|
|
194
|
+
async def refresh_snap(
|
|
195
|
+
self,
|
|
196
|
+
snap: str,
|
|
197
|
+
channel: str = "stable",
|
|
198
|
+
classic: bool = False,
|
|
199
|
+
dangerous: bool = False,
|
|
200
|
+
devmode: bool = False,
|
|
201
|
+
ignore_validation: bool = False,
|
|
202
|
+
jailmode: bool = False,
|
|
203
|
+
revision: int = None,
|
|
204
|
+
filename: str = None,
|
|
205
|
+
wait: bool = False,
|
|
206
|
+
) -> AsyncResponse | ChangesResponse:
|
|
207
|
+
"""
|
|
208
|
+
Refreshes a snap package.
|
|
209
|
+
Args:
|
|
210
|
+
snap (str): The name of the snap package to refresh.
|
|
211
|
+
channel (str, optional): The channel to refresh the snap from. Defaults to "stable".
|
|
212
|
+
classic (bool, optional): Whether to use classic confinement. Defaults to False.
|
|
213
|
+
dangerous (bool, optional): Whether to allow installation of unasserted snaps. Defaults to False.
|
|
214
|
+
devmode (bool, optional): Whether to use development mode. Defaults to False.
|
|
215
|
+
ignore_validation (bool, optional): Whether to ignore validation. Defaults to False.
|
|
216
|
+
jailmode (bool, optional): Whether to use jail mode. Defaults to False.
|
|
217
|
+
revision (int, optional): The specific revision to refresh to. Defaults to None.
|
|
218
|
+
filename (str, optional): The path to the snap file for sideloading. Defaults to None.
|
|
219
|
+
wait (bool, optional): Whether to wait for the refresh operation to complete. Defaults to False.
|
|
220
|
+
Returns:
|
|
221
|
+
AsyncResponse | ChangesResponse: The response from the refresh operation.
|
|
222
|
+
Raises:
|
|
223
|
+
FileNotFoundError: If the specified snap file does not exist.
|
|
224
|
+
ValueError: If attempting to sideload without the dangerous flag set to True.
|
|
225
|
+
SnapdAPIError: If there is an error during the snap refresh.
|
|
226
|
+
"""
|
|
227
|
+
request_data = {
|
|
228
|
+
"action": "refresh",
|
|
229
|
+
"channel": channel,
|
|
230
|
+
"classic": classic,
|
|
231
|
+
"dangerous": dangerous,
|
|
232
|
+
"devmode": devmode,
|
|
233
|
+
"ignore_validation": ignore_validation,
|
|
234
|
+
"jailmode": jailmode,
|
|
235
|
+
}
|
|
236
|
+
if revision:
|
|
237
|
+
request_data["revision"] = revision
|
|
238
|
+
if filename:
|
|
239
|
+
# sideload
|
|
240
|
+
if not Path(filename).exists():
|
|
241
|
+
raise FileNotFoundError(f"File {filename} does not exist")
|
|
242
|
+
if request_data.get("dangerous") is not True:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"Cannot sideload snap without dangerous flag set to True"
|
|
245
|
+
)
|
|
246
|
+
raw_response: httpx.Response = await self._client.request(
|
|
247
|
+
"POST",
|
|
248
|
+
f"{self.common_endpoint}",
|
|
249
|
+
data=request_data,
|
|
250
|
+
files={"snap": open(filename, "rb")},
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
# install from default snap store
|
|
254
|
+
raw_response: httpx.Response = await self._client.request(
|
|
255
|
+
"POST", f"{self.common_endpoint}/{snap}", json=request_data
|
|
256
|
+
)
|
|
257
|
+
response = AsyncResponse.model_validate_json(raw_response.content)
|
|
258
|
+
if wait:
|
|
259
|
+
changes_id = response.change
|
|
260
|
+
previous_changes = None
|
|
261
|
+
while True:
|
|
262
|
+
try:
|
|
263
|
+
changes = await self._client.get_changes_by_id(changes_id)
|
|
264
|
+
logger.debug("Progress: %s", changes.result.overall_progress)
|
|
265
|
+
except httpx.HTTPError:
|
|
266
|
+
if going_to_reload_daemon(previous_changes):
|
|
267
|
+
logger.debug("Waiting for daemon to reload")
|
|
268
|
+
await asyncio.sleep(0.1)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
if changes.ready:
|
|
272
|
+
break
|
|
273
|
+
if changes.result.err:
|
|
274
|
+
raise SnapdAPIError(f"Error in snap remove: {changes.result.err}")
|
|
275
|
+
await asyncio.sleep(0.1)
|
|
276
|
+
previous_changes = changes
|
|
277
|
+
return changes
|
|
278
|
+
return response
|
|
@@ -11,6 +11,7 @@ from snap_python.schemas.store.categories import (
|
|
|
11
11
|
from snap_python.schemas.store.info import VALID_SNAP_INFO_FIELDS, InfoResponse
|
|
12
12
|
from snap_python.schemas.store.search import (
|
|
13
13
|
VALID_SEARCH_CATEGORY_FIELDS,
|
|
14
|
+
ArchSearchResponse,
|
|
14
15
|
SearchResponse,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -165,3 +166,30 @@ class StoreEndpoints:
|
|
|
165
166
|
return await self.find(
|
|
166
167
|
category=category, fields=["title", "store-url", "summary"]
|
|
167
168
|
)
|
|
169
|
+
|
|
170
|
+
async def get_all_snaps_for_arch(self, arch: str) -> ArchSearchResponse:
|
|
171
|
+
# use the old "/api/v1/snaps/names" to get all snaps for a given architecture
|
|
172
|
+
|
|
173
|
+
# ensure valid arch
|
|
174
|
+
if arch not in [
|
|
175
|
+
"amd64",
|
|
176
|
+
"arm64",
|
|
177
|
+
"armhf",
|
|
178
|
+
"i386",
|
|
179
|
+
"ppc64el",
|
|
180
|
+
"s390x",
|
|
181
|
+
"riscv64",
|
|
182
|
+
]:
|
|
183
|
+
raise ValueError(f"Invalid architecture: {arch}")
|
|
184
|
+
|
|
185
|
+
route = "/api/v1/snaps/names"
|
|
186
|
+
extra_headers = {"X-Ubuntu-Architecture": arch}
|
|
187
|
+
|
|
188
|
+
response = await self.store_client.get(
|
|
189
|
+
f"{self._raw_base_url}{route}", headers=extra_headers, timeout=60
|
|
190
|
+
)
|
|
191
|
+
response.raise_for_status()
|
|
192
|
+
|
|
193
|
+
response_json = response.json()
|
|
194
|
+
response_json["arch"] = arch
|
|
195
|
+
return ArchSearchResponse.model_validate(response_json)
|
|
@@ -13,10 +13,17 @@ from snap_python.schemas.common import (
|
|
|
13
13
|
from snap_python.schemas.store.categories import Category
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class SnapHealth(BaseModel):
|
|
17
|
+
status: str
|
|
18
|
+
revision: str
|
|
19
|
+
timestamp: AwareDatetime
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class InstalledSnapFields(Revision):
|
|
17
23
|
apps: list[SnapApp] = Field(default_factory=list)
|
|
18
24
|
developer: Optional[str] = None
|
|
19
25
|
devmode: Optional[bool] = None
|
|
26
|
+
health: Optional[SnapHealth] = None
|
|
20
27
|
icon: Optional[str] = None
|
|
21
28
|
id: Optional[str] = Field(
|
|
22
29
|
None, validation_alias=AliasChoices("id", "snap-id"), serialization_alias="id"
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional
|
|
2
2
|
|
|
3
|
-
from pydantic import
|
|
3
|
+
from pydantic import (
|
|
4
|
+
AliasChoices,
|
|
5
|
+
AliasPath,
|
|
6
|
+
AwareDatetime,
|
|
7
|
+
BaseModel,
|
|
8
|
+
ConfigDict,
|
|
9
|
+
Field,
|
|
10
|
+
)
|
|
4
11
|
|
|
5
12
|
from snap_python.schemas.common import Revision
|
|
6
|
-
from snap_python.schemas.snaps import InstalledSnap, StoreSnap, StoreSnapFields
|
|
13
|
+
from snap_python.schemas.snaps import InstalledSnap, Snap, StoreSnap, StoreSnapFields
|
|
7
14
|
|
|
8
15
|
VALID_SEARCH_CATEGORY_FIELDS = [
|
|
9
16
|
"base",
|
|
@@ -99,15 +106,17 @@ class SearchResult(BaseModel):
|
|
|
99
106
|
revision_info[field] = snap_info.pop(field)
|
|
100
107
|
|
|
101
108
|
snap_id = snap_info.pop("id")
|
|
102
|
-
snap_name = snap_info.
|
|
109
|
+
snap_name = snap_info.get("name")
|
|
103
110
|
|
|
104
111
|
# get set of acceptable fields for StoreSnap
|
|
105
|
-
store_snap_fields = StoreSnapFields.model_fields.keys()
|
|
106
|
-
|
|
112
|
+
store_snap_fields = set(StoreSnapFields.model_fields.keys()).union(
|
|
113
|
+
Snap.model_fields.keys()
|
|
114
|
+
)
|
|
115
|
+
store_snap_info = {
|
|
107
116
|
key: value for key, value in snap_info.items() if key in store_snap_fields
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
snap_info = {"snap":
|
|
119
|
+
snap_info = {"snap": store_snap_info, "snap-id": snap_id, "name": snap_name}
|
|
111
120
|
snap_info["revision"] = revision_info
|
|
112
121
|
|
|
113
122
|
return cls.model_validate(snap_info)
|
|
@@ -118,3 +127,21 @@ class SearchResponse(BaseModel):
|
|
|
118
127
|
|
|
119
128
|
error_list: Optional[List[ErrorListItem]] = Field(None, alias="error-list")
|
|
120
129
|
results: List[SearchResult]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ArchSearchItem(BaseModel):
|
|
133
|
+
model_config = ConfigDict(extra="ignore")
|
|
134
|
+
|
|
135
|
+
aliases: Optional[list[dict[str, str]]]
|
|
136
|
+
apps: list[str]
|
|
137
|
+
package_name: str
|
|
138
|
+
summary: str
|
|
139
|
+
title: str
|
|
140
|
+
version: str
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ArchSearchResponse(BaseModel):
|
|
144
|
+
results: list[ArchSearchItem] = Field(
|
|
145
|
+
alias=AliasPath("_embedded", "clickindex:package")
|
|
146
|
+
)
|
|
147
|
+
arch: str
|
|
@@ -5,13 +5,17 @@ import httpx
|
|
|
5
5
|
from snap_python.schemas.changes import ChangesResponse
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class SnapdAPIError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
8
12
|
class AbstractSnapsClient(ABC): # pragma: no cover
|
|
9
13
|
@abstractmethod
|
|
10
|
-
async def request(self) -> httpx.Response:
|
|
14
|
+
async def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
11
15
|
pass
|
|
12
16
|
|
|
13
17
|
@abstractmethod
|
|
14
|
-
async def request_raw(self) -> httpx.Response:
|
|
18
|
+
async def request_raw(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
|
|
15
19
|
pass
|
|
16
20
|
|
|
17
21
|
@abstractmethod
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/category_schema.json
RENAMED
|
File without changes
|
{snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/snaps_find_schema.json
RENAMED
|
File without changes
|
{snap_python-0.0.4 → snap_python-0.1.0}/src/snap_python/schemas/store/json/snaps_info_schema.json
RENAMED
|
File without changes
|